Say we want to run a snap without involving snapd at all. Can we do that? Of course and it’s quite easy if the snap is a very simple one. Let’s look at the curl snap. First we need to get the snap.
URL=$(curl -s -H 'Snap-Device-Series: 16' http://api.snapcraft.io/v2/snaps/info/curl | jq -r '."channel-map" | map(select(.channel.architecture == "amd64" and .channel.name == "stable")) | .[0].download.url')
wget $URL -O curl.snap
I won’t get into the details of the snap store API but basically we are asking for a JSON document that has URLs in it for a given snap by name, we find the one we want and extract it and then use that to download the snap using wget. Now for the lazy solution we are just going to create a temporary directory where we can mount that snap since it’s just a squashfs file.
CURL_TEMP_DIR=$(mktemp -d)
sudo mount -t squashfs curl.snap $CURL_TEMP_DIR
Substitute $TEMP_DIR for whatever the output of mktemp -d is. Now, what if we just try to run it from that mounted location with no other prep. We know it should be in the bin/ location so let’s try.
tim@pop-os:~/$ $CURL_TEMP_DIR/bin/curl
$CURL_TEMP_DIR/bin/curl: symbol lookup error: $CURL_TEMP_DIR/bin/curl: undefined symbol: curl_easy_header
Well, that doesn’t work but we didn’t really expect it to, did we? No. One thing we need to know is what other files does this snap. For this we look at its base. We can do that in the mounted snap like so
tim@pop-os:~/$ cat /tmp/tmp.MAx7ZUyTMK/snap/snapcraft.yaml |grep "base:"
base: core20
So this snap relies on core20. Let’s get it.
URL=$(curl -s -H 'Snap-Device-Series: 16' http://api.snapcraft.io/v2/snaps/info/core20 | jq -r '."channel-map" | map(select(.channel.architecture == "amd64" and .channel.name == "stable")) | .[0].download.url')
wget $URL -O core20.snap
Let’s repeat the process for it by creating a temporary directory and mounting it.
CORE20_TEMP_DIR=$(mktemp -d)
sudo mount -t squashfs core20.snap $CORE20_TEMP_DIR
Now, let’s do the easiest thing possible to test this setup. We’ll combine the two snaps in a directory and then chroot into it.
CHROOT_TEMP_DIR=$(mktemp -d)
sudo rsync -avr $CORE20_TEMP_DIR/ $CHROOT_TEMP_DIR
sudo rsync -avr $CURL_TEMP_DIR/ $CHROOT_TEMP_DIR
sudo chroot $CHROOT_TEMP_DIR curl
If everything was done correctly we should see the default output for curl.
tim@pop-os:~/gitea/snap_host$ sudo chroot $CHROOT_TEMP_DIR/ /bin/curl
curl: try 'curl --help' or 'curl --manual' for more information
Now, what if we try to get a web page with it?
tim@pop-os:~/gitea/snap_host$ sudo chroot snap_root/ /bin/curl https://google.com
curl: (6) Could not resolve host: google.com
That makes sense. We’ve setup no way for the chroot to resolve name. What about if we just provide it an IP address.
tim@pop-os:~/gitea/snap_host$ sudo chroot snap_root/ /bin/curl https://1.1.1.1
If we do that then we should see it successfull retrieve the page from that server.
So in conclusion a snap is really just some files bundled together in the squashfs format that relies on some other base bundle of files in another squashfs that are all expected to be merged together in a filesystem that is subsquently used as the root filesystem for a container-like setup.
So what’s next? Well, let’s create a snap host in Rust that basically does all of this with one command.
And naturally when I say a snap “is really just….” I am oversimplifying as much as humanly possible. There are a ton of things you have to account for when trying to make a snap host but that is functionality provided by snapd. Fundamentally a snap is just a squashfs file.