Pinning: Apt’s Best Kept Secret


I have recently discovered a feature of the apt(8) package manager that smooths over almost all of the annoyances that I've had with point-release distributions (Debian, Ubuntu, etc.). apt(8) provides a simple and effective way to install newer versions of packages than the ones included in the release version that you are running: pinning.[1]

First, a general observation about package versions: For most packages installed on a Linux system, the fact that they aren't the latest version is not very important. Ubuntu and Debian backport security and bug fixes to the supported versions of software for their major releases. This means you may miss out on some new features, but theoretically shouldn't be forced to live with any serious bugs or security vulnerabilities. It is only a small number of packages for which it is important to get the latest and greatest version. What those packages are is going to be pretty personal. For me, it's mpv(1), yt-dlp(1) and nvim(1). If you're into gaming, graphics drivers will probably make the list.

With this in mind, pinning in apt(8) is a feature that lets you control what repository apt(8) uses to install specific packages. There are two configuration files that we will be managing here: /etc/apt/sources.list and /etc/apt/preferences. The first of these files lists the repositories that apt(8) will pull packages from, and the second allows you to set priorities for these repositories, either on a package by package basis, or in general.

Let's consider getting an up-to-date version of yt-dlp(1) on Ubuntu 22.04 LTS as a specific example. Recently, YouTube pushed out an API update that broke this program, and the authors of yt-dlp(1) pushed an update to unbreak it. This updated package is not (or at least was not) available in the standard repositories for 22.04. [2] However, it is available in the repositories for the upcoming Ubuntu 23.04, Lunar Lobster. So let's configure apt(8) to install Lunar Lobster's version of yt-dlp(1) onto a machine running Jammy Jellyfish.


Ubuntu requires using specific releases (either by version number or codename) when configuring repositories for pinning. This is in contrast to Debian, which allows the use of generic stable, testing, and unstable labels that always point to the current Debian release corresponding to the label (although doing this is considered bad practice).

This means that this post will be out of date within a few months. Throughout this post, jammy should be replaced with the codename for the version of Ubuntu you are currently running, and lunar with the codename for the version you want packages from.

Adding new Repositories

First, we'll perform a full update of the packages installed on our system. We're going to attempt to do an update later to verify that we have configured our preferences correctly, so we want to make sure that we're fully up to date going in [3],

                sudo apt update
                sudo apt upgrade
            

Now, we need to update our /etc/apt/sources.list to add the repositories for Lunar Lobster. This is as simple as adding the following line to the file,

deb http://us.archive.ubuntu.com/ubuntu lunar main restricted universe multiverse

We now have added the Lunar repositories to apt(8). To verify this, run another update, and then use apt-cache(8) to verify that lunar is available. You should see a long list of the repositories that apt(8) is configured to interact with, which will include Lunar's.

                sudo apt update
                sudo apt-cache policy
...
500 http://us.archive.ubuntu.com/ubuntu lunar/multiverse amd64 Packages
    release v=23.04,o=Ubuntu,a=lunar,n=lunar,l=Ubuntu,c=multiverse,b=amd64
    origin us.archive.ubuntu.com
500 http://us.archive.ubuntu.com/ubuntu lunar/universe amd64 Packages
    release v=23.04,o=Ubuntu,a=lunar,n=lunar,l=Ubuntu,c=universe,b=amd64
    origin us.archive.ubuntu.com
500 http://us.archive.ubuntu.com/ubuntu lunar/restricted amd64 Packages
    release v=23.04,o=Ubuntu,a=lunar,n=lunar,l=Ubuntu,c=restricted,b=amd64
    origin us.archive.ubuntu.com
500 http://us.archive.ubuntu.com/ubuntu lunar/main amd64 Packages
    release v=23.04,o=Ubuntu,a=lunar,n=lunar,l=Ubuntu,c=main,b=amd64
    origin us.archive.ubuntu.com
...

The number in front of the repositories is the priority, which apt(8) uses to determine where to get packages when installing or updating. By default, you should see 500 used both here, and for Jammy's repositories. Generally speaking, higher numbers mean higher priority, though the details are a bit more complicated than that (we'll get there later).

The upshot of this default configuration is that, because lunar and jammy both have the same priority, apt(8) will prefer to install the most up-to-date package available from either of them. In practice, this means that lunar will be the default repository for your system. You can verify this by starting an apt upgrade; it will attempt to upgrade nearly every package on your system, even though we ensured it was fully up-to-date before getting here. Abort the upgrade--we aren't trying to update to a different version of Ubuntu right now.

Setting Repository Priorities

This behavior is great if it is your intent to upgrade to Lunar Lobster, but isn't so great if you only want to install a couple of packages from there. So, we need to adjust the priorities such that Jammy's repositories are preferred over Lunar's for general usage.

The way that you set the priority for repositories is by either creating an /etc/apt/preferences file, or adding files to /etc/apt/preferences.d, and specifying priorities in there. We'll go the single-file route. Create (if it doesn't already exist) /etc/apt/preferences and place the following inside it,

Package: *
Pin: release n=jammy
Pin-Priority: 700
 
Package: *
Pin: release n=lunar
Pin-Priority: -10

and then run apt update again to load the new preferences. If you attempt an apt upgrade after this, it shouldn't try to install any updates from the lunar repositories.

The first part of each section of this file specifies the packages that the configuration applies to. You can configure this on a package by package basis, or use *, as we do here, to apply the settings to all packages. If you did want to do this for a specific package, simply replace the * with the package name.

Then, we specify the repository using Pin: release n=x. If you look back at the output from apt-cache policy, you'll see that each repository has a couple of parameters listed, an n, a, o, etc. You can use any of these to specify which repository or repositories you are referring to with this setting. a is the most specific, so a=jammy will refer specifically to the jammy repository, but not jammy-updates or jammy-security, whereas n=jammy will collectively reference all three.

Finally, the Pin-Priority sets the priority of the specified repositories for the specific packages. The general rule is that the highest priority wins when trying to install or update, but it's actually a bit more complicated than that. The details are in the manual (apt_preferences(5)), but I'll repeat them here. Note that P refers to the priority number.

P >= 1000
   causes a version to be installed even if this constitutes a
   downgrade of the package

990 <= P > 1000 causes a version to be installed even if it does not come from the target release, unless the installed version is more recent
500 <= P > 990 causes a version to be installed unless there is a version available belonging to the target release or the installed version is more recent
100 <= P > 500 causes a version to be installed unless there is a version available belonging to some other distribution or the installed version is more recent
0 <P > 100 causes a version to be installed only if there is no installed version of the package
P < 0 prevents the version from being installed
P = 0 has undefined behavior, do not use it.

We'll discuss what a "target release" is next, but first some general comments. Priorities between 500 and 989 are the ones that behave as you probably expect: if all your repositories are in this range, the package will be installed and/or updated from the highest priority repository that has it. If there's a tie, the repository with the newest version wins. You will never downgrade a package--so if one is installed from a lower priority repository it won't get replaced by an older version in a higher priority repository.

The other priority ranges have special properties. If you set a priority over 1000, this will cause packages from that repository to be used, even if it results in a downgrade. It isn't often that this is what you want, so you want to generally stay away from this. Priorities below 0 will cause packages to never be installed, unless you manually set the repository as a target.

In this example, we've set the package priority for lunar to -10, which means that packages will never get installed from there. If we try to install a package that is in lunar, but not in jammy, the install will fail. If you were to instead set lunar to a priority of 600, then jammy will be the default source for packages, but if a package is not in jammy, lunar will be checked for it. I find this useful in Debian, where stable is sometimes missing things that you may want to install, but haven't needed it in Ubuntu as of yet.

Installing a Package from a Specific Repository

We have blocked installing packages from lunar by default. If we want to install something from there, we can force it by setting lunar as the target repository when we call apt(8). As an example, at the time of this writing the yt-dlp(1) package is on version 2022.04.08 in the jammy repositories. So if we install it, we'll see,

            sudo apt install yt-dlp
            yt-dlp--version
            2022.04.08
        

This version is actually broken now, because YouTube has updated its API and so trying to use it will result in an error,

            yt-dlp https://www.youtube.com/watch?v=NQ5uD5x8vzg
[youtube] NQ5uD5x8vzg: Downloading webpage
[youtube] NQ5uD5x8vzg: Downloading android player API JSON ERROR:
[youtube] NQ5uD5x8vzg: Unable to extract uploader id; please report
this issue on https://github.com/yt-dlp/yt-dlp/issues?q= ,
filling out the appropriate issue template. Confirm you are
on the latest version using yt-dlp -U 
        

However, we can forcibly install a newer version from lunar by using the -t flag in apt(8) to specify an install target repository,

            sudo apt install -t lunar yt-dlp
            yt-dlp --version
            2023.02.17
            yt-dlp https://www.youtube.com/watch?v=NQ5uD5x8vzg
            actually works now!
        

By specifying the target, we override the block on using packages from lunar. Further, this package will now be updated based on the repository it was installed from, so when a new version is pushed to lunar, apt(8) will download it without us needing to do anything special.

There is one caveat to this. If the package you install has any dependencies, these may be satisfied from lunar if necessary. This means that any other packages that share this dependency will need to be updated to their lunar versions too. If you later attempt to install a package that depends on an older version of one of these dependencies, apt(8) will fail. This is because the installed package will come from the jammy repositories, and the dependencies installed on your system will be the newer ones from lunar. Unless jammy is set with a priority greater than 1000, apt(8) won't downgrade these dependencies to match the package you're installing, and so it won't be able to install the package. Should this happen, you need to install the version of this package from lunar instead, using the -t lunar flag in the install command.

Rolling Everything Back


Downgrading packages can have unexpected effects, and may cause things to break. Be careful about doing this. But, if you horribly break something, and are considering a reinstall anyway, it may be worth trying this first.

If you decide that you regret everything and want to roll back to the default jammy versions of all of your packages, apt(8) can do that, and it probably won't break your system. All you need to do is bump the jammy entry in your /etc/apt/preferences file to have a priority of over 1000, and apt(8) will force all the packages it can into compliance. This may result in some packages being uninstalled, if apt(8) can't make it work out.

More specifically, update /etc/apt/preferences so that it contains,

Package: *
Pin: release n=jammy
Pin-Priority: 1100
 
Package: *
Pin: release n=lunar
Pin-Priority: -10

and then run,

            sudo apt update
            sudo apt upgrade
        

This will cause apt(8) to downgrade packages as necessary to ensure that they all are coming from the jammy repositories.

Conclusion

Pinning packages with apt(8) is an incredibly useful feature, and has changed the way that I look at "stable" distributions like Ubuntu LTS and Debian. Using this feature, you can have a stable base system, without losing access to new versions of specific software that must be kept up to date, or without missing out on the latest features in some userland applications. In a sense, it gets you the best of both worlds: rolling release for selective userspace applications on top of a stable base system.

I used Arch Linux for over a decade before I learned about pinning, and now I'm happily running Debian as my main Linux distribution. Selectively installed packages from the unstable branch let me me have the latest toys, but I don't have to deal with the instability of running the core system components under a rolling release model.

apt(8) converted me away from Arch Linux--I just had to learn how to actually use it first.