I am one of the developers of Guardian Eye, a .NET/Avalonia desktop application that uses FFmpeg, OpenCV and ONNX Runtime and talks to USB and network cameras. We recently packaged the Linux build as a strictly confined snap.
The final snap is about 121 MB. Getting there exposed a few problems that did not show up in small Snapcraft examples.
AppStream metadata overwrote the Store Listing
We keep the public listing in the Snapcraft Dashboard and had Update metadata on release disabled. Even so, one upload removed screenshots, the featured banner and part of the manually edited listing.
The snap included an AppStream file and the manifest used parse-info. That put AppStream fields, including a links: block, into meta/snap.yaml. Uploading the revision then changed the saved Store Listing despite the Dashboard setting.
We still package the AppStream file, because it is useful to desktop catalogs and a future Flatpak build, but Snapcraft no longer parses it. adopt-info is used only to set the version with craftctl set version.
Two CI checks now enforce this: one rejects parse-info and optional title metadata in snapcraft.yaml; the other inspects the finished snap and rejects a links: block in meta/snap.yaml before upload.
The CI runner ran out of disk before Snapcraft finished
The failure looked like this:
No space left on device
It happened while copying libonnxruntime_providers_cuda.so.
Our normal self-contained Linux publish included the GPU builds of ONNX Runtime and OpenVINO. Together they added roughly 725 MB of native files. The direct .deb/archive build can use those packages, but the snap cannot install the matching GPU runtime into its read-only filesystem, so copying them into the snap build was wasted work.
The snap-specific MSBuild profile now excludes those package references at build time. A build guard fails if either provider appears in the snap publish graph again.
We also had two complete linux-x64 trees on the runner: one from the direct Linux packages and another built with the Snap-specific compile defines. Before producing the snap payload we delete the previous RID-specific build output. After dotnet publish, we keep the payload but remove the redundant self-contained output before Snapcraft expands it again under parts/, stage/ and prime/.
A successful link did not prove that the payload was complete
The snap uses core24 plus the gnome-46-2404 and mesa-2404 runtime snaps. Some dependencies that look like ordinary system libraries, including X11 client libraries, are supplied by those mounted runtimes rather than by the application payload.
Our CI now installs all three runtime snaps and builds an index of the libraries they expose. It then finds every ELF file in the application payload by magic bytes, reads its NEEDED entries with readelf, and requires each soname to resolve from one of four places:
the directory containing the ELF file
the payload root
core24, gnome-46-2404 or mesa-2404
a small, explicitly justified allowlist
This check runs on the post-processed payload, not on raw dotnet publish output. The raw output still contains optional providers and tracing components that are intentionally removed before packaging.
Strict confinement turned hardware access into release work
The application needs more than camera. It also declares audio-record, hardware-observe, removable-media and password-manager-service.
hardware-observe is the non-obvious one. The camera interface exposes the video device and basic vendor/product information, but not the USB serial number or /sys/class/sound. Without those paths, a camera moved to another USB port can look like a new device and pairing it with its microphone becomes unreliable.
Sensitive interfaces require separate auto-connection declarations. Connecting them manually proves that the application can use them, but does not prove that the installed experience is ready for ordinary users. Revision review and interface auto-connection review are separate concerns.
The most useful diagnostics have been:
snap connections guardian-eye
journalctl -k | grep 'apparmor="DENIED"' | grep guardian-eye
The full hardware test matrix is still in progress. So far we have verified installation from the Store branch, application startup, and camera discovery activity in the logs after manually connecting the required interfaces. We are not treating that as proof that USB identity, multicast receive, network-share recording and GPU fallback all work yet.
A named branch works for staging, but it is not private
Our staging build goes to:
latest/edge/stage
Testers must know the branch name and select it explicitly:
sudo snap install guardian-eye --channel=latest/edge/stage
The branch is unlisted and expires after a period without new releases, but anyone who knows its full name can install it. It is useful for short-lived Store-delivery tests, not for distributing secrets or genuinely private builds.
For production we use snapcraft upload --release=stable. In our workflow the upload passes the Store’s automatic revision review and is released to the channel in the same operation. There is no separate step where that already uploaded revision is attached to an app version. Manual review can still happen for flagged revisions and interface declarations.
The resulting Store page is here for context: