Publishing snaps from macOS (multipass + snapcraft)
Snap packages can only be built on Linux — snapcraft needs snapd, which needs a Linux kernel. On macOS the official path is to spin up a Linux VM via multipass. This page collects the sharp edges you'll hit the first time, especially around:
- Multipass networking on macOS (the "No route to host" problem even when
pingworks). - The snap-store chicken-and-egg where a registered-but-never-published name refuses
snapcraft upload. - Authentication via exported store credentials instead of interactive login.
Multipass: "No route to host" on multipass exec / multipass transfer
Symptoms
$ multipass exec snap-bootstrap -- true
exec failed: ssh connection failed: 'Failed to connect: No route to host'
$ multipass transfer ./somefile snap-bootstrap:/home/ubuntu/
ssh connection failed: 'Failed to connect: No route to host'
Even though:
$ multipass list
Name State IPv4 Image
snap-bootstrap Running 192.168.2.2 Ubuntu 24.04 LTS
$ ping -c 3 192.168.2.2
3 packets transmitted, 3 packets received, 0.0% packet loss
$ nc -vz 192.168.2.2 22
Connection to 192.168.2.2 port 22 [tcp/ssh] succeeded!
The VM is up, the network is fine, sshd is listening — only multipassd's internal SSH channel is refusing to connect. This is a multipass daemon bug, not a networking problem.
Fixes, in order of effort
1. Restart the multipass daemon
Usually the fastest fix:
sudo launchctl kickstart -k system/com.canonical.multipassd
sleep 3
multipass exec snap-bootstrap -- true
2. Stop and start the VM
If the daemon restart alone isn't enough:
multipass stop snap-bootstrap
multipass start snap-bootstrap
sleep 10
multipass exec snap-bootstrap -- true
3. Bypass multipass exec entirely — SSH directly
When ping and nc -vz 22 both succeed but multipass exec still fails, you can skip the daemon's channel and SSH straight to the VM using multipass's own private key:
sudo cat "/var/root/Library/Application Support/multipassd/ssh-keys/id_rsa" > /tmp/mp.key
chmod 600 /tmp/mp.key
ssh -i /tmp/mp.key -o StrictHostKeyChecking=no ubuntu@192.168.2.2
The ubuntu user is multipass's default. Once inside, you have a normal shell and can do anything multipass exec would have done. For one-off file transfers while the daemon is wedged:
# Copy INTO the VM via ssh
scp -i /tmp/mp.key ./localfile ubuntu@192.168.2.2:/home/ubuntu/
# Or pipe
cat localfile | ssh -i /tmp/mp.key ubuntu@192.168.2.2 'cat > /home/ubuntu/file'
Cleanup the key when done:
rm /tmp/mp.key
4. Last resort — purge and relaunch
If none of the above works the VM may be irrecoverably wedged:
multipass delete snap-bootstrap && multipass purge
multipass launch --name snap-bootstrap --cpus 2 --memory 4G --disk 10G 24.04
Fresh VMs almost always work cleanly on the first multipass exec after launch.
Alternative workflow: use the repo mount
Before fighting multipass-exec, remember that multipass launch auto-mounts your home directory (or you can multipass mount an arbitrary path). Once the mount exists, host-side writes are immediately visible inside the VM without needing multipass transfer:
multipass mount "$(pwd)" snap-bootstrap:/home/ubuntu/project
# put whatever you need on the host side:
cp /tmp/secret.txt ./.temp/secret.txt
# and reference it inside the VM:
ssh -i /tmp/mp.key ubuntu@192.168.2.2 'ls /home/ubuntu/project/.temp/secret.txt'
Gitignore the staging path so it doesn't leak into source control.
Snap Store: registered-but-never-published names
Symptoms
Uploading a brand-new snap fails with:
Store operation failed:
- resource-not-found: Snap not found for name=<your-snap-name>
This happens on both host-side (snapcraft upload after snapcraft login) and inside CI (goreleaser's snapcraft upload --release=edge,beta …).
Cause
A snap name goes through two distinct states in the store:
- Registered — reserved via
snapcraft registeror the dashboard. The name is yours, but no revision has ever been uploaded. - Published — at least one revision exists. The name is now a real snap with a metadata page, channel map, etc.
The snapcraft upload endpoint (and the ACLs that credentials mint against) treat these differently. A registered-only name can't be the target of upload from any client — the store returns resource-not-found. You need to make the first upload through a path that transitions the name from registered to published.
The dashboard's "Publish to this name" link next to the registered name just opens the snapcraft publishing docs — it is not a wizard that activates the name. Ignore that button.
Fix: one manual bootstrap upload from Linux
Any other upload attempt (CLI, CI, goreleaser) will keep failing with the same error until a first revision exists. Do it exactly once, from a real Linux host.
-
Export store credentials on the host (the browser flow works where the docker image's email/password flow doesn't):
snapcraft export-login \
--acls=package_access,package_push,package_update,package_release \
--expires=2030-01-01 /tmp/snap-credentials.txtOmit
--snaps=<name>— the scoping requires the snap to already be published. Omit--channelsso the credential works for any channel. -
Get the credentials + a built
.snapinto a Linux VM. Multipass is the common choice on macOS:multipass launch --name snap-bootstrap --cpus 2 --memory 4G --disk 10G 24.04
multipass mount "$(pwd)" snap-bootstrap:/home/ubuntu/project
cp /tmp/snap-credentials.txt ./.temp/snap-credentials.txt -
Inside the VM, build and upload:
sudo snap install snapcraft --classic
sudo snap install go --classic
sudo snap install goreleaser --classic --edge # or wget the binary
export SNAPCRAFT_STORE_CREDENTIALS="$(cat /home/ubuntu/project/.temp/snap-credentials.txt)"
cd /home/ubuntu/project
git checkout <your-tag>
goreleaser release --snapshot --clean --skip=publish # produces dist/*.snap
snapcraft upload --release=edge dist/<name>_*_linux_amd64.snapExpected output:
Revision 1 of '<name>' createdandReleased to edge. -
Done. The name is now published. Subsequent
snapcraft uploadcalls (from CI, from your laptop, from wherever) will succeed because the name is in the published state.
Caveats
-
Classic confinement gates
stable, notedge/beta. If your snap usesconfinement: classic, the first upload enqueues a one-time manual review by the snap store team before the snap can be released to thestablechannel. Until that clears, onlyedge/beta/candidatework. Request review at forum.snapcraft.io/c/store-requests/19. -
grade: develalso blockscandidateandstablechannels — devel revisions only release toedgeandbeta. Use a goreleaser template to flip grade at the right semver cut-over:grade: '{{ if eq .Major 0 }}devel{{ else }}stable{{ end }}' -
snapcraft in docker (
snapcore/snapcraft:stable) can't complete the browser-based 2FA login flow. Use multipass, a cloud Linux VM, or GitHub Actions for the bootstrap instead of the container.
goreleaser + snapcraft: useful non-obvious flags
snapshot.version_template: "{{ incpatch .Version }}-next"in.goreleaser.yamlis whygoreleaser release --snapshotproduces0.10.3-nextand not the current tag. Tweak the template or pass--snapshotfrom the actual tag commit to get the version you want in artifact filenames.goreleaser release --snapshot --clean --skip=publishbuilds everything including.snapwithout uploading — good for local dry-runs and the bootstrap flow above.publish: falseundersnapcrafts:builds the.snapbut doesn't try to upload to the store. Useful while you're waiting on name activation or classic-confinement review so unrelated release channels still succeed.
Verifying a successful snap publish
snap info <your-snap-name> --color=never | head -30
# Should show "snap-id", "publisher", and a channels section with the revision.
# From any Linux:
sudo snap install <your-snap-name> --classic --edge # or --stable once grade flips
# From the snap store API:
curl -s "https://api.snapcraft.io/v2/snaps/info/<your-snap-name>" \
-H 'Snap-Device-Series: 16' | jq '.channel-map[] | {channel, revision, version}'