Snap package: strict core24 snap on every release, optional Snap Store upload (#413)

* Snap package: build, test and attach a strict snap to every release

packaging/linux/snap/snapcraft.yaml (core24, strict, gnome extension) repackages
the release's Linux tarball like the Flatpak does. The release workflow builds it
for amd64 and arm64, installs it with --dangerous, models a box in the confined
app on headless X11 and Wayland, and attaches solidcraft-<version>-linux-<arch>.snap
plus the version-less copy. When SNAPCRAFT_STORE_CREDENTIALS is set, nightlies go
to edge and release drafts to candidate, and snap-store.yml promotes candidate to
stable when a release is published. The README gets a Snap badge and install line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

* snap smoke: stop the app's whole process group, refuse a busy control port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

* snap smoke: quit the app cleanly between backends

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-12 18:07:26 -04:00
1 parent cddbfedb28
commit daed2686bc
10 files changed
+484 -4

No files matched your search

+2 -1
View File
@@ -32,7 +32,8 @@ old-name Pages addresses are not permanent aliases.
- **macOS 11+** (Apple silicon and Intel): open the `.dmg` and drag `SolidCraft.app` onto Applications. - **macOS 11+** (Apple silicon and Intel): open the `.dmg` and drag `SolidCraft.app` onto Applications.
The CLI is in `solidcraft-cli-{version}-macos-universal.zip`. The CLI is in `solidcraft-cli-{version}-macos-universal.zip`.
- **Linux** (x86_64 and aarch64, glibc 2.35+): the `.AppImage` (`chmod +x`, then run it), the - **Linux** (x86_64 and aarch64, glibc 2.35+): the `.AppImage` (`chmod +x`, then run it), the
`.deb` or `.rpm`, the `.flatpak` (`flatpak install --user <file>`), or the `.tar.gz`. `.deb` or `.rpm`, the `.flatpak` (`flatpak install --user <file>`), the `.snap`
(`sudo snap install --dangerous <file>`, or `sudo snap install solidcraft` from the Snap Store), or the `.tar.gz`.
- **FreeBSD:** `tar -xzf solidcraft-{version}-freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local`. - **FreeBSD:** `tar -xzf solidcraft-{version}-freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local`.
- **Web:** `solidcraft-web-{version}.zip` is a static site to host yourself. - **Web:** `solidcraft-web-{version}.zip` is a static site to host yourself.
- `SHA256SUMS.txt`: `sha256sum -c SHA256SUMS.txt --ignore-missing`. - `SHA256SUMS.txt`: `sha256sum -c SHA256SUMS.txt --ignore-missing`.
+2
View File
@@ -57,3 +57,5 @@ jobs:
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.solvecraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solvecraft.metainfo.xml" sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.solvecraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solvecraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.solvecraft.metainfo.xml" appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.solvecraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.solvecraft.bundle.yml')); assert m['id'] == 'ai.storyteller.solvecraft' and m['command'] == 'solidcraft'" python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.solvecraft.bundle.yml')); assert m['id'] == 'ai.storyteller.solvecraft' and m['command'] == 'solidcraft'"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/snap/snapcraft.yaml')); assert m['name'] == 'solidcraft' and m['confinement'] == 'strict' and m['apps']['solidcraft']['command'] == 'usr/bin/solidcraft'"
python3 -m py_compile packaging/linux/snap-smoke.py
+84 -2
View File
@@ -6,7 +6,7 @@
# At midnight Eastern, changed main commits build and publish a nightly pre-release. # At midnight Eastern, changed main commits build and publish a nightly pre-release.
# A version with a suffix (0.2.0-rc.1) is a pre-release. Nothing is signed yet. # A version with a suffix (0.2.0-rc.1) is a pre-release. Nothing is signed yet.
# #
# Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .tar.gz packaging/linux # Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .snap, .tar.gz packaging/linux
# FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd # FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd
# macOS universal .dmg, and the CLI as a .zip packaging/macos # macOS universal .dmg, and the CLI as a .zip packaging/macos
# Windows x64, x86, arm64 .msi and a portable .zip; x64 also the plain .exe packaging/windows # Windows x64, x86, arm64 .msi and a portable .zip; x64 also the plain .exe packaging/windows
@@ -199,6 +199,59 @@ jobs:
snap:
name: Snap ${{ matrix.arch }}
needs: [version, linux]
if: needs.version.outputs.build == 'true'
# Repackages the Linux job's tarball, like the Flatpak. On 24.04 because the core24 base is
# Ubuntu 24.04, which lets snapcraft build on the runner itself (--destructive-mode).
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- { arch: x86_64, runner: ubuntu-24.04 }
- { arch: aarch64, runner: ubuntu-24.04-arm }
env:
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release-linux-${{ matrix.arch }}
path: dist/linux
- name: Install snapcraft and the smoke-test tools
run: |
sudo snap install snapcraft --classic
sudo apt-get update
sudo apt-get install -y --no-install-recommends xvfb xauth weston dbus-x11
- name: Build the snap
run: |
sudo --preserve-env=SOLIDCRAFT_VERSION,SOLIDCRAFT_BUILD_DATE env "PATH=$PATH" \
packaging/linux/snap-pack.sh --destructive-mode "dist/linux/solidcraft-$SOLIDCRAFT_VERSION-linux-${{ matrix.arch }}.tar.gz"
sudo chown -R "$(id -u):$(id -g)" dist target
- name: Install it and start the app on headless X11 and Wayland
run: |
snap_file="dist/release/solidcraft-$SOLIDCRAFT_VERSION-linux-${{ matrix.arch }}.snap"
sudo snap install --dangerous "$snap_file"
snap info --verbose "$snap_file" | head -n 12
snap connections solidcraft
solidcraft.cli --version
# Snaps expect the login session's runtime directory, which runners don't create.
sudo install -d -m 700 -o "$(id -u)" -g "$(id -g)" "/run/user/$(id -u)"
python3 packaging/linux/snap-smoke.py --out snap-smoke
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: snap-smoke-${{ matrix.arch }}
path: snap-smoke/*.png
if-no-files-found: error
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-snap-${{ matrix.arch }}
path: dist/release/*.snap
if-no-files-found: error
freebsd: freebsd:
name: FreeBSD x86_64 name: FreeBSD x86_64
needs: version needs: version
@@ -337,7 +390,7 @@ jobs:
release: release:
name: GitHub Release or dry-run artifacts name: GitHub Release or dry-run artifacts
needs: [version, linux, flatpak, freebsd, macos, windows, web] needs: [version, linux, flatpak, snap, freebsd, macos, windows, web]
if: needs.version.outputs.build == 'true' if: needs.version.outputs.build == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -425,6 +478,35 @@ jobs:
echo echo
sed 's/^/ /' SHA256SUMS.txt sed 's/^/ /' SHA256SUMS.txt
} >>"$GITHUB_STEP_SUMMARY" } >>"$GITHUB_STEP_SUMMARY"
# Snap Store: nightlies go to the edge channel and release drafts to candidate, only after
# every package built and the GitHub release was staged. Publishing the GitHub release
# promotes candidate to stable (snap-store.yml). Dry runs never upload. Skipped until the
# owner adds the SNAPCRAFT_STORE_CREDENTIALS secret (docs/releasing.md).
- id: snapstore
name: Check for Snap Store credentials
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
run: |
if [ -n "$SNAPCRAFT_STORE_CREDENTIALS" ]; then
echo "configured=true" >>"$GITHUB_OUTPUT"
else
echo "No SNAPCRAFT_STORE_CREDENTIALS secret: skipping the Snap Store upload."
fi
- name: Upload the snaps to the Snap Store
if: >-
steps.snapstore.outputs.configured == 'true' && (github.event_name == 'schedule' ||
(github.ref == 'refs/heads/release' && (github.event_name == 'push' || inputs.draft)))
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
working-directory: artifacts
run: |
sudo snap install snapcraft --classic
channel=candidate
if [ "$GITHUB_EVENT_NAME" = schedule ]; then channel=edge; fi
for arch in x86_64 aarch64; do
snapcraft upload --release="$channel" "solidcraft-$VERSION-linux-$arch.snap"
done
echo "Uploaded the snaps to the Snap Store's $channel channel." >>"$GITHUB_STEP_SUMMARY"
- name: Keep the files (dry run) - name: Keep the files (dry run)
if: github.event_name == 'workflow_dispatch' && !inputs.draft if: github.event_name == 'workflow_dispatch' && !inputs.draft
run: cp notes.md artifacts/release-notes.md run: cp notes.md artifacts/release-notes.md
+40
View File
@@ -0,0 +1,40 @@
# Publishing a GitHub release (not a prerelease) promotes the snaps that release.yml uploaded to
# the Snap Store's candidate channel to stable, so `sudo snap install solidcraft` gets it.
# Skipped until the owner adds the SNAPCRAFT_STORE_CREDENTIALS secret; see docs/releasing.md.
name: Snap Store
on:
release:
types: [released]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: snap-store
cancel-in-progress: false
jobs:
promote:
runs-on: ubuntu-latest
steps:
- id: creds
name: Check for Snap Store credentials
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
run: |
if [ -n "$SNAPCRAFT_STORE_CREDENTIALS" ]; then
echo "configured=true" >>"$GITHUB_OUTPUT"
else
echo "No SNAPCRAFT_STORE_CREDENTIALS secret: nothing to promote."
fi
- name: Promote candidate to stable
if: steps.creds.outputs.configured == 'true'
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
run: |
sudo snap install snapcraft --classic
snapcraft status solidcraft
snapcraft promote solidcraft --from-channel=latest/candidate --to-channel=latest/stable --yes
snapcraft status solidcraft
+5
View File
@@ -15,6 +15,7 @@ model through the built-in MCP server.
[![Download for Windows][badge-windows]][windows-msi] [![Download for Windows][badge-windows]][windows-msi]
[![Download for macOS][badge-macos]][macos-dmg] [![Download for macOS][badge-macos]][macos-dmg]
[![Download for Linux][badge-linux]][linux-appimage] [![Download for Linux][badge-linux]][linux-appimage]
[![Get it from the Snap Store][badge-snap]][snap-store]
[![Try it in your browser][badge-web]][web-app] [![Try it in your browser][badge-web]][web-app]
- **Windows:** download and run the [installer][windows-msi]. - **Windows:** download and run the [installer][windows-msi].
@@ -25,6 +26,7 @@ model through the built-in MCP server.
- **Linux:** download the [AppImage][linux-appimage], allow it to run as a program and open it. - **Linux:** download the [AppImage][linux-appimage], allow it to run as a program and open it.
Or install the [.deb][linux-deb] (Ubuntu, Debian, Mint) or [.rpm][linux-rpm] (Fedora, Or install the [.deb][linux-deb] (Ubuntu, Debian, Mint) or [.rpm][linux-rpm] (Fedora,
openSUSE), or use [Flatpak](https://bherbruck.github.io/solidcraft/guide/getting-started.html#linux-flatpak). openSUSE), or use [Flatpak](https://bherbruck.github.io/solidcraft/guide/getting-started.html#linux-flatpak).
With Snap: `sudo snap install solidcraft` ([Snap Store][snap-store], or the [.snap file][linux-snap]).
For ARM64: [AppImage][linux-arm64-appimage], [.deb][linux-arm64-deb], [.rpm][linux-arm64-rpm]. For ARM64: [AppImage][linux-arm64-appimage], [.deb][linux-arm64-deb], [.rpm][linux-arm64-rpm].
- **Web:** [try it in your browser][web-app], no install. - **Web:** [try it in your browser][web-app], no install.
@@ -38,6 +40,7 @@ Install details, upgrading from earlier builds and building from source are in
[badge-windows]: https://img.shields.io/badge/Windows-Download-0078D6?style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0yIDJoOS41djkuNUgyek0xMi41IDJIMjJ2OS41aC05LjV6TTIgMTIuNWg5LjVWMjJIMnpNMTIuNSAxMi41SDIyVjIyaC05LjV6Ii8%2BPC9zdmc%2B [badge-windows]: https://img.shields.io/badge/Windows-Download-0078D6?style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0yIDJoOS41djkuNUgyek0xMi41IDJIMjJ2OS41aC05LjV6TTIgMTIuNWg5LjVWMjJIMnpNMTIuNSAxMi41SDIyVjIyaC05LjV6Ii8%2BPC9zdmc%2B
[badge-macos]: https://img.shields.io/badge/macOS-Download-333333?style=for-the-badge&logo=apple&logoColor=white [badge-macos]: https://img.shields.io/badge/macOS-Download-333333?style=for-the-badge&logo=apple&logoColor=white
[badge-linux]: https://img.shields.io/badge/Linux-AppImage-E95420?style=for-the-badge&logo=linux&logoColor=white [badge-linux]: https://img.shields.io/badge/Linux-AppImage-E95420?style=for-the-badge&logo=linux&logoColor=white
[badge-snap]: https://img.shields.io/badge/Snap-Snap_Store-82BEA0?style=for-the-badge&logo=snapcraft&logoColor=white
[badge-web]: https://img.shields.io/badge/Web-Try_in_browser-2EA44F?style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxwYXRoIGQ9Ik0yIDEyaDIwTTEyIDJhMTUgMTUgMCAwIDEgMCAyME0xMiAyYTE1IDE1IDAgMCAwIDAgMjAiLz48L3N2Zz4%3D [badge-web]: https://img.shields.io/badge/Web-Try_in_browser-2EA44F?style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxwYXRoIGQ9Ik0yIDEyaDIwTTEyIDJhMTUgMTUgMCAwIDEgMCAyME0xMiAyYTE1IDE1IDAgMCAwIDAgMjAiLz48L3N2Zz4%3D
[windows-msi]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-windows-x64.msi [windows-msi]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-windows-x64.msi
[windows-arm64-msi]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-windows-arm64.msi [windows-arm64-msi]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-windows-arm64.msi
@@ -50,6 +53,8 @@ Install details, upgrading from earlier builds and building from source are in
[linux-arm64-appimage]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-linux-aarch64.AppImage [linux-arm64-appimage]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-linux-aarch64.AppImage
[linux-arm64-deb]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-linux-aarch64.deb [linux-arm64-deb]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-linux-aarch64.deb
[linux-arm64-rpm]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-linux-aarch64.rpm [linux-arm64-rpm]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-linux-aarch64.rpm
[linux-snap]: https://github.com/bherbruck/solidcraft/releases/latest/download/solidcraft-linux-x86_64.snap
[snap-store]: https://snapcraft.io/solidcraft
[web-app]: https://bherbruck.github.io/solidcraft/app/ [web-app]: https://bherbruck.github.io/solidcraft/app/
**Documentation:** the [SolidCraft book](https://bherbruck.github.io/solidcraft/) (user guide, **Documentation:** the [SolidCraft book](https://bherbruck.github.io/solidcraft/) (user guide,
+16
View File
@@ -51,6 +51,22 @@ missing runtime. Launch with `flatpak run ai.storyteller.solvecraft`, rather tha
a host `solidcraft` binary. Files chosen through Open/Save use the desktop file-chooser portal; a host `solidcraft` binary. Files chosen through Open/Save use the desktop file-chooser portal;
command-line paths outside Documents may need a specific additional filesystem permission. command-line paths outside Documents may need a specific additional filesystem permission.
### Linux Snap
On Ubuntu and other distributions with snapd:
```sh
sudo snap install solidcraft
solidcraft # or open SolidCraft from the app menu
solidcraft.cli --version # the CLI, for scripts and `solidcraft.cli mcp`
```
The snap can open designs in your home folder (not hidden folders) and, through the file
chooser, anywhere else. For USB drives and other mounted disks, run
`sudo snap connect solidcraft:removable-media` once. A release's `.snap` file installs without
the store with `sudo snap install --dangerous ./solidcraft-VERSION-linux-ARCH.snap`; it then
doesn't update automatically.
## Web ## Web
The same app runs in the browser: [open SolidCraft on the web](../app/) (`?sample` opens the sample The same app runs in the browser: [open SolidCraft on the web](../app/) (`?sample` opens the sample
+50 -1
View File
@@ -15,7 +15,7 @@ Every release asset, binary, installer product name and package uses the SolidCr
copies, the deprecated old-name CLI alias); keep them until that document says they can go. Do not copies, the deprecated old-name CLI alias); keep them until that document says they can go. Do not
rewrite published assets, checksums, tags or historical release notes of earlier versions. rewrite published assets, checksums, tags or historical release notes of earlier versions.
The main installers (`.msi`, portable `.zip`, `.dmg`, `.AppImage`, `.deb`, `.rpm`, `.flatpak`) are The main installers (`.msi`, portable `.zip`, `.dmg`, `.AppImage`, `.deb`, `.rpm`, `.flatpak`, `.snap`) are
also attached without the version in their names, for example `solidcraft-windows-x64.msi` and also attached without the version in their names, for example `solidcraft-windows-x64.msi` and
`solidcraft-linux-x86_64.AppImage`, and listed in `SHA256SUMS.txt`. The README's Download section `solidcraft-linux-x86_64.AppImage`, and listed in `SHA256SUMS.txt`. The README's Download section
links to `https://github.com/bherbruck/solidcraft/releases/latest/download/<name>`, which follows links to `https://github.com/bherbruck/solidcraft/releases/latest/download/<name>`, which follows
@@ -145,6 +145,55 @@ docs updates include the latest stable Flatpak release too. Verify the resulting
not only the dispatch job, so a release cannot erase the docs or app and docs updates cannot not only the dispatch job, so a release cannot erase the docs or app and docs updates cannot
erase the package repository. erase the package repository.
## Snap package and Snap Store
Every release and nightly attaches `solidcraft-<version>-linux-<arch>.snap` for x86_64 and
aarch64 (`packaging/linux/snap-pack.sh`, manifest `packaging/linux/snap/snapcraft.yaml`). Like the
Flatpak, it repackages the Linux job's tarball. The `snap` job installs it with `--dangerous`,
starts the confined app on headless X11 and Wayland (`packaging/linux/snap-smoke.py`), models a
box over the control channel and keeps the captured frames as the `snap-smoke-<arch>` artifact.
The Snap Store steps run only when the `SNAPCRAFT_STORE_CREDENTIALS` repository secret exists;
without it they are skipped and the `.snap` files are still attached to the release:
- nightlies are uploaded to `latest/edge`;
- release drafts (pushes to `release`) are uploaded to `latest/candidate`, after every package
built and the draft was staged;
- publishing a non-prerelease GitHub release runs `snap-store.yml`, which promotes
`latest/candidate` to `latest/stable`. As with the Flatpak repository, a publication made with
`GITHUB_TOKEN` doesn't trigger it: then run `gh workflow run snap-store.yml`.
To enable the Snap Store once (owner):
1. Sign in at <https://snapcraft.io> with an Ubuntu One account and register the name at
<https://snapcraft.io/register-snap> (or `snapcraft login` then `snapcraft register solidcraft`).
2. On a machine with snapcraft (`sudo snap install snapcraft --classic`), export upload-only
credentials for this snap:
```sh
snapcraft export-login --snaps=solidcraft \
--channels=edge,candidate,stable \
--acls=package_access,package_push,package_update,package_release \
--expires=2027-10-01 solidcraft-store-credentials.txt
```
3. Store them as the repository secret and delete the file:
```sh
gh secret set SNAPCRAFT_STORE_CREDENTIALS -R bherbruck/solidcraft < solidcraft-store-credentials.txt
rm solidcraft-store-credentials.txt
```
4. Optional: fill in the store listing at <https://snapcraft.io/solidcraft/listing> (the icon,
summary and description come from the snap; add screenshots and the category "Art and design").
5. The next nightly fills `edge` and the next release fills `candidate`, then `stable` on
publication. To fill `stable` from an already published release right away, download its
`.snap` files and run `snapcraft upload --release=stable <file>` for each.
Renew the credentials before `--expires` by repeating steps 2 and 3. The snap uses strict
confinement and only plugs that connect automatically, apart from `removable-media`, which users
connect with `sudo snap connect solidcraft:removable-media` (getting-started.md).
## Repository setup ## Repository setup
The `release` branch is advanced only for intentional release batches. Restrict its writers to The `release` branch is advanced only for intentional release batches. Restrict its writers to
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Repackage a SolidCraft Linux tarball as a snap:
#
# $DIST/solidcraft-<version>-linux-<arch>.snap
#
# Usage: packaging/linux/snap-pack.sh [--destructive-mode] [TARBALL]
#
# TARBALL defaults to $DIST/solidcraft-<version>-linux-<arch>.tar.gz from package.sh. The snap is
# built for the host architecture (x86_64 or aarch64), which must match the tarball's. Needs
# snapcraft (`sudo snap install snapcraft --classic`) and, without --destructive-mode, LXD.
# --destructive-mode builds on the host itself, which must be Ubuntu 24.04 (the core24 base),
# as in CI. Install and smoke-test the result with:
# sudo snap install --dangerous dist/release/solidcraft-<version>-linux-<arch>.snap
# python3 packaging/linux/snap-smoke.py
# Manifest: packaging/linux/snap/snapcraft.yaml.
set -euo pipefail
# shellcheck source=../env.sh
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
HERE="$ROOT/packaging/linux"
MODE=(--use-lxd)
TARBALL=""
while [ $# -gt 0 ]; do
case "$1" in
--destructive-mode) MODE=(--destructive-mode); shift ;;
-h | --help) sed -n '2,15p' "$0"; exit 0 ;;
-*) echo "unknown argument: $1" >&2; exit 2 ;;
*) TARBALL="$1"; shift ;;
esac
done
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) SNAP_ARCH=amd64 ;;
aarch64 | arm64) ARCH=aarch64; SNAP_ARCH=arm64 ;;
*) echo "unsupported architecture $ARCH" >&2; exit 2 ;;
esac
BASENAME="solidcraft-$VERSION-linux-$ARCH"
TARBALL="${TARBALL:-$DIST/$BASENAME.tar.gz}"
[ -f "$TARBALL" ] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; }
command -v snapcraft >/dev/null || { echo "error: snapcraft not found (sudo snap install snapcraft --classic)" >&2; exit 1; }
# Snap versions: at most 32 characters of [A-Za-z0-9.+~-].
if ! printf '%s' "$VERSION" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9.+~-]{0,31}$'; then
echo "error: version $VERSION is not a valid snap version" >&2
exit 1
fi
echo "==> SolidCraft $VERSION snap for $SNAP_ARCH from $(basename "$TARBALL")"
WORK="$CARGO_TARGET_DIR/snap-pack"
rm -rf "$WORK"
mkdir -p "$WORK/snap" "$WORK/stage"
tar -xzf "$TARBALL" -C "$WORK/stage" --strip-components=1
[ -x "$WORK/stage/bin/solidcraft" ] || { echo "error: $TARBALL has no bin/solidcraft" >&2; exit 1; }
cp "$HERE/snap/snapcraft.yaml" "$WORK/snap/snapcraft.yaml"
printf '%s\n' "$VERSION" >"$WORK/version.txt"
(cd "$WORK" && snapcraft pack "${MODE[@]}" --build-for="$SNAP_ARCH" --output="$WORK/out.snap")
OUT="$DIST/$BASENAME.snap"
mv "$WORK/out.snap" "$OUT"
echo "wrote $OUT"
ls -lh "$OUT"
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""Start the installed SolidCraft snap on headless X11 and Wayland and model a box in it.
Runs the confined app (`snap install --dangerous` first) with its normal plugs, drives it over
the control channel from the host (snaps share the host's network namespace), checks a
30x20x10 box measures 6000 mm³ and that the GPU drew a non-blank frame, and copies each frame
to --out (default: the current directory) as snap-<backend>.png. Needs xvfb-run and weston.
Reuses the frame checks of flatpak-smoke.py.
"""
import argparse
import importlib.util
import json
import os
from pathlib import Path
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
import time
HERE = Path(__file__).resolve().parent
_spec = importlib.util.spec_from_file_location("flatpak_smoke", HERE / "flatpak-smoke.py")
flatpak_smoke = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(flatpak_smoke)
PORTS = {"x11": 18839, "wayland": 18840}
REQUESTS = [
{"id": 1, "method": "ui.inspect", "params": {}},
{"id": 2, "method": "engine.execute",
"params": {"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}},
{"id": 3, "method": "document.inspect", "params": {"measure": True}},
]
def drive(app, port, frame):
"""Connect to the app's control port, model the box and capture a frame."""
deadline = time.monotonic() + 60
while True:
if app.poll() is not None:
raise RuntimeError(f"the app exited with {app.returncode} before listening")
try:
conn = socket.create_connection(("127.0.0.1", port), timeout=60)
break
except OSError:
if time.monotonic() >= deadline:
raise RuntimeError("the control port never opened") from None
time.sleep(.2)
with conn, conn.makefile("rw") as io:
replies = []
for request in REQUESTS + [{"id": 4, "method": "ui.screenshot", "params": {"path": str(frame)}},
{"id": 5, "method": "app.quit", "params": {"force": True}}]:
io.write(json.dumps(request) + "\n")
io.flush()
replies.append(io.readline())
renderer = flatpak_smoke.validate_replies("".join(replies[:3]))
capture = json.loads(replies[3])
if any(json.loads(reply).get("ok") is not True for reply in replies[4:]):
raise ValueError(f"app.quit failed: {replies[4:]}")
if capture.get("ok") is not True or renderer != "gpu":
raise ValueError(f"GPU frame capture failed: renderer={renderer}, {capture}")
return renderer, flatpak_smoke.frame_statistics(frame.read_bytes())
def run_backend(backend, out):
runtime = Path(f"/run/user/{os.getuid()}")
if not runtime.is_dir():
raise RuntimeError(f"{runtime} is missing: snaps expect the login session's runtime directory")
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime))
for name in ("WAYLAND_DISPLAY", "WAYLAND_SOCKET", "DISPLAY", "SOLIDCRAFT_WAYLAND", "SOLVECRAFT_WAYLAND"):
env.pop(name, None)
# The snap can write to the (non-hidden) home folder, not to the host's /tmp.
work = Path(tempfile.mkdtemp(prefix="solidcraft-snap-smoke-", dir=Path.home()))
compositor = None
log = (work / "app.log").open("wb")
try:
# No private dbus-run-session bus: snapd moves the app into its own systemd scope through
# the session's systemd, which a private bus doesn't have.
prefix = []
if backend == "x11":
prefix += ["xvfb-run", "-a", "-s", "-screen 0 1600x1000x24 -nolisten tcp"]
else:
# The wayland plug only allows sockets named wayland-N in the runtime directory.
env["WAYLAND_DISPLAY"] = "wayland-77"
compositor = subprocess.Popen(["weston", "--backend=headless", "--renderer=pixman", "--no-config",
"--socket=wayland-77", "--width=1600", "--height=1000"],
env=env, stdout=log, stderr=subprocess.STDOUT)
deadline = time.monotonic() + 20
while not (runtime / "wayland-77").exists():
if compositor.poll() is not None or time.monotonic() >= deadline:
raise RuntimeError("headless Weston failed")
time.sleep(.1)
with socket.socket() as probe:
if probe.connect_ex(("127.0.0.1", PORTS[backend])) == 0:
raise RuntimeError(f"port {PORTS[backend]} is already in use (another SolidCraft?)")
# Its own process group, so stopping it also stops the app under xvfb-run.
app = subprocess.Popen(prefix + ["/snap/bin/solidcraft", "--control", str(PORTS[backend])],
env=env, stdout=log, stderr=subprocess.STDOUT, start_new_session=True)
try:
renderer, stats = drive(app, PORTS[backend], work / "frame.png")
# app.quit exits cleanly, so the next run doesn't report a crash.
app.wait(timeout=30)
finally:
if app.poll() is None:
os.killpg(app.pid, signal.SIGTERM)
try:
app.wait(timeout=15)
except subprocess.TimeoutExpired:
os.killpg(app.pid, signal.SIGKILL)
app.wait()
shutil.copy(work / "frame.png", out / f"snap-{backend}.png")
print(f"snap {backend}: PASS renderer={renderer}, box=6000 mm³, frame={stats}")
except BaseException:
log.flush()
sys.stderr.write((work / "app.log").read_text(errors="replace")[-8000:])
raise
finally:
log.close()
if compositor:
compositor.terminate()
compositor.wait(timeout=10)
shutil.rmtree(work, ignore_errors=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--backend", choices=["x11", "wayland", "both"], default="both")
parser.add_argument("--out", type=Path, default=Path.cwd())
args = parser.parse_args()
args.out.mkdir(parents=True, exist_ok=True)
for backend in (["x11", "wayland"] if args.backend == "both" else [args.backend]):
run_backend(backend, args.out.resolve())
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, KeyError, TypeError, RuntimeError, subprocess.SubprocessError) as error:
print(f"snap desktop smoke failed: {error}", file=sys.stderr)
sys.exit(1)
+81
View File
@@ -0,0 +1,81 @@
# Snap package (solidcraft-<version>-linux-<arch>.snap on every GitHub release, and the Snap
# Store when its credentials are configured). Build it with packaging/linux/snap-pack.sh.
#
# Like the Flatpak, it repackages the release's own Linux tarball (built on Ubuntu 22.04 by
# package.sh) instead of compiling Rust: the same binaries as the AppImage/deb/rpm. snap-pack.sh
# unpacks the tarball into stage/ next to snap/ and writes the package version to version.txt.
# The binaries need glibc >= 2.35 (core24 has 2.39) and load X11, Wayland, xkbcommon and Vulkan
# at runtime: the gnome extension supplies the desktop libraries and the GPU userspace (mesa,
# through the gpu-2404 content interface) for the wgpu viewport.
name: solidcraft
title: SolidCraft
summary: Open-source parametric 3D CAD
description: |
SolidCraft is an open-source parametric 3D CAD application written in Rust. Sketch on a
plane, constrain and dimension the sketch, turn profiles into solids on a feature timeline,
and change a parameter to watch the timeline rebuild.
- Sketches with a constraint solver and parameter-driven dimensions
- Extrude, revolve, sweep, loft, fillet, chamfer, shell, holes and patterns
- Components and joints, sheet metal, and a plastic-enclosure subset
- STEP and IGES import and export; 3MF, STL and OBJ
- Every action is a command, scriptable from a CLI, a JSON control channel and an MCP server
The command-line tool is `solidcraft.cli` (for example `solidcraft.cli mcp`).
adopt-info: solidcraft
license: MIT OR Apache-2.0
contact: https://github.com/bherbruck/solidcraft/issues
website: https://bherbruck.github.io/solidcraft/
source-code: https://github.com/bherbruck/solidcraft
issues: https://github.com/bherbruck/solidcraft/issues
icon: stage/share/icons/hicolor/512x512/apps/ai.storyteller.solvecraft.png
base: core24
grade: stable
confinement: strict
# lzo starts faster than the default xz, at a somewhat larger download.
compression: lzo
platforms:
amd64:
arm64:
apps:
solidcraft:
command: usr/bin/solidcraft
desktop: usr/share/applications/ai.storyteller.solvecraft.desktop
# Ties the snap to the AppStream id (ai.storyteller.solvecraft, a compatibility shim kept by
# every package; see docs/compatibility.md) so software centres treat it as the same app.
common-id: ai.storyteller.solvecraft
extensions: [gnome]
plugs:
- desktop
- desktop-legacy
- wayland
- x11
- opengl
# Designs anywhere in the home folder (not hidden files) and on USB drives; Open/Save
# also work through the desktop file-chooser portal.
- home
- removable-media
# Opening links (Create Issue, documentation); the optional --control server listens
# on 127.0.0.1 for agents and tests.
- network
- network-bind
cli:
command: usr/bin/solidcraft-cli
plugs: [home, removable-media, network, network-bind]
parts:
solidcraft:
plugin: dump
source: stage
organize:
bin/: usr/bin/
share/: usr/share/
override-pull: |
craftctl default
craftctl set version="$(cat "$CRAFT_PROJECT_DIR/version.txt")"
override-prime: |
craftctl default
# Point the menu entry at the packaged icon (the hicolor name isn't on the host's path).
sed -i 's|^Icon=.*|Icon=${SNAP}/usr/share/icons/hicolor/256x256/apps/ai.storyteller.solvecraft.png|' \
usr/share/applications/ai.storyteller.solvecraft.desktop