|
| 1 | +In this tutorial you will build a snap package for a Python application called [liquitctl](https://github.com/liquidctl/liquidctl) using Snapcraft, which is the build ecosystem for creating, publishing and maintaining snaps. |
| 2 | + |
| 3 | +The concepts covered in this tutorial are applicable to all snaps, regardless of their complexity. We'll cover everything from creating the build environment and the configuration file, to troubleshooting missing libraries and which interfaces may be required. |
| 4 | + |
| 5 | +- [Requirements](#heading--requirements) |
| 6 | +- 1\. [Snapcraft setup](#heading--setup) |
| 7 | + * 1.1 [Snapcraft build environment](#heading--build-environment) |
| 8 | + * 1.2 [Create a YAML template](#heading--yaml) |
| 9 | + * 1.3 [Build a template snap](#heading--build-template) |
| 10 | +- 2\. [Modify the snapcraft.yaml](#heading--modify) |
| 11 | + * 2.1 [Create a new part](#heading--part) |
| 12 | + * 2.2 [Build the part](#heading--build-part) |
| 13 | +- 3\. [Create an app section](#heading--expose) |
| 14 | + * 3.1 [Install the snap in developer mode](#heading--developer-mode) |
| 15 | +- 4\. [Test the snap](#heading--test) |
| 16 | + * 4.1 [Missing dependencies](#heading--missing) |
| 17 | + * 4.2 [System access](#heading--system-access) |
| 18 | +- 5\. [Update the confinement level](#heading--confinement) |
| 19 | + |
| 20 | +<a id='heading--requirements'></a> |
| 21 | + |
| 22 | +### Requirements |
| 23 | + |
| 24 | +Snapcraft can be installed on various Linux distributions, as well as on macOS and Windows operating systems. For this tutorial, however, we recommend using [Ubuntu 22.04 LTS (Jammy Jellyfish)](https://releases.ubuntu.com/jammy/) or later. |
| 25 | + |
| 26 | +This tutorial does not require any programming or specific Linux knowledge, but you will need some familiarity with the Linux command line. All the instructions are run as commands from the [Terminal](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) application. |
| 27 | + |
| 28 | +You system also needs to have at least 20GB of storage available. |
| 29 | + |
| 30 | +<a id='heading--setup'></a> |
| 31 | + |
| 32 | +## 1. Snapcraft setup |
| 33 | + |
| 34 | +From the terminal, type the following to install Snapcraft: |
| 35 | + |
| 36 | +```bash |
| 37 | +sudo snap install snapcraft --classic |
| 38 | +``` |
| 39 | + |
| 40 | +<h3 id='heading--build-environment'>1.1 Snapcraft build environment</h3> |
| 41 | + |
| 42 | +Snapcraft builds snaps within an [LXD](https://ubuntu.com/lxd) container environment by default. This keeps a snap build isolated from your system and ensures that any dependencies the snap requires are only provided by the build process. |
| 43 | + |
| 44 | +To install LXD, type the following: |
| 45 | + |
| 46 | +```bash |
| 47 | +sudo snap install lxd |
| 48 | +``` |
| 49 | + |
| 50 | +You also need to add your current user to the `lxd` group to give yourself permission to access its resources: |
| 51 | + |
| 52 | +```bash |
| 53 | +sudo usermod -a -G lxd $USER |
| 54 | +``` |
| 55 | + |
| 56 | +Logout and re-open your user session for the new group to become active. |
| 57 | + |
| 58 | +LXD can now be initialised with the 'lxd init' command: |
| 59 | + |
| 60 | +```bash |
| 61 | +lxd init --minimal |
| 62 | +``` |
| 63 | + |
| 64 | +See [How to install LXD](https://documentation.ubuntu.com/lxd/en/latest/installing/#installing) for further installation options and troubleshooting. |
| 65 | + |
| 66 | +<h3 id='heading--yaml'>1.2 Create a YAML template</h3> |
| 67 | + |
| 68 | +Start by creating a new directory to hold the snap data, and then `cd` into this directory: |
| 69 | + |
| 70 | +```bash |
| 71 | +mkdir mysnap |
| 72 | +cd mysnap |
| 73 | +``` |
| 74 | + |
| 75 | +To create a new YAML template for a working snap, run `snapcraft init` within this directory: |
| 76 | + |
| 77 | +``` |
| 78 | +snapcraft init |
| 79 | +``` |
| 80 | + |
| 81 | +The YAML template file is called `snapcraft.yaml` and it can be found within a new `snap` sub-directory. |
| 82 | + |
| 83 | +<h3 id='heading--build-template'>1.3 Build a template snap</h3> |
| 84 | + |
| 85 | +The template file contains enough information to build a snap without any further modifications. This can be accomplished by running the `snapcraft` command in the parent directory: |
| 86 | + |
| 87 | +``` |
| 88 | +snapcraft |
| 89 | +``` |
| 90 | + |
| 91 | +In the background, Snapcraft will create a new LXD container, install into this whatever the template file contains, and build a snap. The output will look similar to the following and the resultant snap can be found in the current directory: |
| 92 | + |
| 93 | +```bash |
| 94 | +Launching instance... |
| 95 | +Executed: pull my-part |
| 96 | +Executed: build my-part |
| 97 | +Executed: stage my-part |
| 98 | +Executed: prime my-part |
| 99 | +Executed parts lifecycle |
| 100 | +Generated snap metadata |
| 101 | +Created snap package my-snap-name_0.1_amd64.snap |
| 102 | +``` |
| 103 | + |
| 104 | +<h2 id='heading--modify'>2. Modify the snapcraft.yaml</h2> |
| 105 | + |
| 106 | +The `snap/snapcraft.yaml` file describes the application, its dependencies and how it should be built. It currently contains the following metadata: |
| 107 | + |
| 108 | +```yaml |
| 109 | +name: my-snap-name # you probably want to 'snapcraft register <name>' |
| 110 | +base: core22 # the base snap is the execution environment for this snap |
| 111 | +version: '0.1' # just for humans, typically '1.2+git' or '1.3.2' |
| 112 | +summary: Single-line elevator pitch for your amazing snap # 79 char long summary |
| 113 | +description: | |
| 114 | + This is my-snap's description. You have a paragraph or two to tell the |
| 115 | + most important story about your snap. Keep it under 100 words though, |
| 116 | + we live in tweetspace and your description wants to look good in the snap |
| 117 | + store. |
| 118 | +
|
| 119 | +grade: devel # must be 'stable' to release into candidate/stable channels |
| 120 | +confinement: devmode # use 'strict' once you have the right plugs and slots |
| 121 | + |
| 122 | +parts: |
| 123 | + my-part: |
| 124 | + # See 'snapcraft plugins' |
| 125 | + plugin: nil |
| 126 | +``` |
| 127 | +
|
| 128 | +The above metadata is enough to build a snap, but the snap has no functionality. To create a functional snap, we need expand the `parts:` section and add a new section called `app:`. |
| 129 | + |
| 130 | +<h3 id='heading--part'>2.1 Create a new part</h2> |
| 131 | + |
| 132 | +A snap is assembled from one or more _parts_ and each part describes a component required for the snap to function. This component could be a library or an executable, for example, and parts use _plugins_ to construct and organise whatever components are needed. |
| 133 | + |
| 134 | +Our application is built with Python, and Snapcraft includes a [Python plugin](/t/the-python-plugin/8529) to automatically handle its dependencies and install requirements. |
| 135 | + |
| 136 | +Open `snap/snapcraft.yaml` with your favourite text editor and navigate to the bottom line, `plugin: nil`. Replace `nil` with `python` and add the following `source-type` and `source` lines: |
| 137 | + |
| 138 | +```yaml |
| 139 | + plugin: python |
| 140 | + source-type: git |
| 141 | + source: https://github.com/liquidctl/liquidctl |
| 142 | +``` |
| 143 | + |
| 144 | +This is all that is required for Snapcraft to access, clone locally, and build the upstream source code of the project. |
| 145 | + |
| 146 | +Running `snapcraft` again would build the application and create a new snap. However, this new snap would still not function because we have not yet told Snapcraft which executable to expose and run. |
| 147 | + |
| 148 | +<h3 id='heading--build-part'>2.2 Build the part</h3> |
| 149 | + |
| 150 | +A snap is built in several stages, collectively known as the _parts lifecycle_, as shown in Snapcraft's build output. |
| 151 | + |
| 152 | +1. **Pull** retrieves whatever is required for each part to be built |
| 153 | +1. **Build** constructs each part using each respective plugins |
| 154 | +1. **Stage** copies built components into a shared staging area |
| 155 | +1. **Prime** moves staged files and directories into their final locations |
| 156 | + |
| 157 | +This is important because you can stop a build at any stage to look inside the build container. |
| 158 | + |
| 159 | +Run the following `snapcraft` command to both start a new snap build and run the build up to the _prime_ step. The command will also open a shell within the build environment. |
| 160 | + |
| 161 | +```bash |
| 162 | +snapcraft prime --shell |
| 163 | +``` |
| 164 | + |
| 165 | +> :information_source: If you've already built the same snap, run `snapcraft clean` first to reset the build environment. |
| 166 | + |
| 167 | + |
| 168 | +From the build shell prompt inside the container, type `cd $HOME` to change to Snapcraft's build directory, and `ls` to see its contents: |
| 169 | + |
| 170 | +``` |
| 171 | +environment.sh parts prime project snap stage |
| 172 | +``` |
| 173 | + |
| 174 | +These directories hold the data for each build stage, while the `environments.sh` file contains the environment variable configuration. |
| 175 | + |
| 176 | +The executable name is `liquidctl`, which we can now search for: |
| 177 | + |
| 178 | +```bash |
| 179 | +$ find . -name liquidctl |
| 180 | +./project/squashfs-root/bin/liquidctl |
| 181 | +./parts/my-part/build/build/lib/liquidctl |
| 182 | +./parts/my-part/build/liquidctl |
| 183 | +./parts/my-part/src/liquidctl |
| 184 | +./parts/my-part/install/bin/liquidctl |
| 185 | +./parts/my-part/install/lib/python3.10/site-packages/liquidctl |
| 186 | +./stage/bin/liquidctl |
| 187 | +./stage/lib/python3.10/site-packages/liquidctl |
| 188 | +``` |
| 189 | + |
| 190 | +The above output shows how the Python plugin has built and installed the executable within the container. The final binary is in `./stage/bin`. |
| 191 | + |
| 192 | +Type `exit` to quit the build environment shell. |
| 193 | + |
| 194 | +<a id='heading--expose'></a> |
| 195 | + |
| 196 | +## 3. Create an app section |
| 197 | + |
| 198 | +Using the location of the binary, and to permit access, it needs to be declared within an `app:` section of the snapcraft.yaml: |
| 199 | + |
| 200 | +```yaml |
| 201 | +apps: |
| 202 | + my-snap-name: |
| 203 | + command: bin/liquidctl |
| 204 | +``` |
| 205 | + |
| 206 | +If the sub-section name matches the snap name it becomes the default executable for the snap. |
| 207 | + |
| 208 | +This means that when our snap is installed, typing `my-snap-name` will run the `bin/liquidctl` binary . It's more usual for a snap name to match the name of the executable. |
| 209 | + |
| 210 | +<h3 id='heading--developer-mode'>3.1 Install the snap in developer mode</h3> |
| 211 | + |
| 212 | +The snap can now be rebuilt to produce what should be an installable and executable snap package. |
| 213 | + |
| 214 | +Running `snapcraft` will produce a snap package called `my-snap-name_0.1_amd64.snap` (depending on your system architecture). |
| 215 | + |
| 216 | +```bash |
| 217 | +Created snap package my-snap-name_0.1_amd64.snap |
| 218 | +``` |
| 219 | + |
| 220 | +This snap package can be installed locally with the snap command, invoking both `--devmode` and `--dangerous` options to permit system access and installation without verification: |
| 221 | + |
| 222 | +```bash |
| 223 | +sudo snap install ./my-snap-name_0.1_amd64.snap --dangerous --devmode |
| 224 | +``` |
| 225 | + |
| 226 | +With the snap installed, the `my-snap-name` command can now be run to execute `liquidctl`: |
| 227 | + |
| 228 | +```bash |
| 229 | +$ my-snap-name |
| 230 | +Usage: |
| 231 | + liquidctl [options] list |
| 232 | + liquidctl [options] initialize [all] |
| 233 | +[...] |
| 234 | +``` |
| 235 | + |
| 236 | +<a id='heading--test'></a> |
| 237 | + |
| 238 | +## 4. Test the snap |
| 239 | + |
| 240 | +To test a snap properly, it needs to be run as intended. The `liquidctl` command, for example, accesses USB devices to read and set proprietary sensor, fan and LEDs values. |
| 241 | + |
| 242 | +Even without such devices connected, the `list` will attempt to discover any connected devices: |
| 243 | + |
| 244 | +```bash |
| 245 | +$ my-snap-name list |
| 246 | +usb.core.NoBackendError: No backend available |
| 247 | +``` |
| 248 | + |
| 249 | +This produces an error, and unless you know the project code, it's difficult to say from the error whether it's a problem with the snap, a problem with not having the hardware, or a problem with our test system. |
| 250 | + |
| 251 | +<h3 id='heading--missing'>4.1 Missing dependencies</h3> |
| 252 | + |
| 253 | +If you're a Python developer, or reasonably good at searching the internet, it's relatively straightforward to work out that the `usb.core.NoBackendError` issue is caused by a missing `python3-usb` package. This can be added through a new `stage-packages` section for the part. Stage packages are those packages you wish to be installed alongside the application: |
| 254 | + |
| 255 | +```yaml |
| 256 | + stage-packages: |
| 257 | + - python3-usb |
| 258 | +``` |
| 259 | + |
| 260 | +After building and installing the new snap, the error will have gone. If you had any compatible devices, you would now see output similar to the following: |
| 261 | + |
| 262 | +```bash |
| 263 | + Device #0: Corsair HX750i |
| 264 | + Device #1: Corsair Hydro H100i v2 |
| 265 | +``` |
| 266 | + |
| 267 | +<h3 id='heading--system-access'>4.2 System access</h3> |
| 268 | + |
| 269 | +Running our snap with real hardware will result in an insufficient permissions error and this is because snaps limit system access by default. [Interfaces](/t/supported-interfaces/7744) are used to permit access to individual resources through _plugs_ and _slots_. |
| 270 | + |
| 271 | +Plugs declares which [interfaces](/t/supported-interfaces/7744) an app needs to function, such as [home](/t/the-home-interface/7838) to access local files, or [network](/t/the-network-interface/7880) to access the network. In this case, _liquidctl_ needs access to USB devices, which can be satisfied with the [raw-usb](/t/the-raw-usb-interface/7908) interface for device input and output, [uhid](/t/the-uhid-interface/7931) for user access, and [hardware-observe](/t/the-hardware-observe-interface/7833) to enable the system to see which devices are connected. |
| 272 | + |
| 273 | +These can added with the creation of a new `plugs:` sections beneath the command name for the app: |
| 274 | + |
| 275 | +``` |
| 276 | +apps: |
| 277 | + my-snap-name: |
| 278 | + command: bin/liquidctl |
| 279 | + plugs: |
| 280 | + - raw-usb |
| 281 | + - uhid |
| 282 | + - hardware-observe |
| 283 | +``` |
| 284 | + |
| 285 | +When the snap is installed, the interfaces are can be activated manually with the following commands: |
| 286 | + |
| 287 | + |
| 288 | +```bash |
| 289 | +sudo snap connect my-snap-name:uhid |
| 290 | +sudo snap connect my-snap-name:raw-usb |
| 291 | +sudo snap connect my-snap-name:hardware-observe |
| 292 | +``` |
| 293 | + |
| 294 | +The snap can now be run without encountering any further errors or missing functionality. |
| 295 | + |
| 296 | +<a id='heading--confinement'></a> |
| 297 | + |
| 298 | +## 5. Update confinement level |
| 299 | + |
| 300 | +The final step when building any snap is to change its grade to `stable` and its confinement to `strict`. Both of these values are at the top of the snapcraft.yaml file and they default to developer-friendly options so that errors only report themselves rather than stop functionality. They're useful when building a snap but are far less secure when you want to share it. |
| 301 | + |
| 302 | +```yaml |
| 303 | +grade: stable # must be 'stable' to release into candidate/stable channels |
| 304 | +confinement: strict # use 'strict' once you have the right plugs and slots |
| 305 | +``` |
| 306 | + |
| 307 | +The snap is now fully functional and can be rebuilt and installed. At this point, your own snaps could be published and shared. |
0 commit comments