cd ../writing

Using Arch Metapackages to Manage Project Dependencies

Use small Arch metapackages to declare a project's native development dependencies, so pacman can safely remove what is no longer needed.

On this page
  1. A metapackage for a GTK application
  2. Removing the project environment
  3. One metapackage per project
  4. Each project describes what it actually needs
  5. Direct and transitive dependencies
  6. Build tools belong in the environment too
  7. What about packages that were already installed?
  8. Why not just install everything with --asdeps?
  9. Why I prefer this approach
  10. A small package with a useful job

When developing software on Arch Linux, it is easy to accumulate packages that are only needed for a particular project.

A GTK application, for example, might require GTK itself, libadwaita, VTE, Blueprint Compiler, and various other libraries or development tools. Installing those packages is easy:

sh
sudo pacman -S gtk4 libadwaita vte4 blueprint-compiler

The less obvious question comes later: which of those packages can I safely remove when I stop working on the project?

I have started using small Arch metapackages to solve this problem.

Rather than treating a project’s dependencies as a collection of packages I happen to have installed, I create a package representing the development environment itself. Pacman can then keep track of the relationships for me.

A metapackage for a GTK application

I’m currently developing an Adwaita-based terminal in Rust. Its development environment needs GTK 4, libadwaita, VTE 4, and Blueprint Compiler.

I represent that environment with this PKGBUILD:

makefile
# Maintainer: Joshua Rosato <[email protected]>

pkgname=adw-vte-dev
pkgver=1.0
pkgrel=1
pkgdesc="Metapackage pinning the GTK4 / libadwaita / VTE stack for Adwaita terminal development"
arch=('any')
license=('custom')

depends=(
  'gtk4'
  'libadwaita'
  'vte4'
  'blueprint-compiler'
)

package() {
  :
}

There is no software in the package itself. Its only purpose is to declare dependencies.

After building and installing it, the relationship looks roughly like this:

text
adw-vte-dev
├── gtk4
├── libadwaita
├── vte4
└── blueprint-compiler

Those packages are no longer just unrelated things installed on my machine. They are dependencies of a package whose purpose I understand: my terminal development environment.

Removing the project environment

The main advantage appears when I no longer need the environment.

Instead of trying to remember which libraries I installed months ago, I can remove the metapackage:

sh
sudo pacman -Rns adw-vte-dev

Pacman already knows the dependency graph.

If a dependency was installed for this package and nothing else needs it, it can be removed along with the metapackage.

If another installed package still requires it, it stays.

For example, imagine another application also depends on libadwaita:

text
                ┌── gtk4
                │
adw-vte-dev ────┼── libadwaita ◄──── some-other-package
                │
                ├── vte4
                │
                └── blueprint-compiler

Removing adw-vte-dev does not mean blindly removing everything in its dependency list. libadwaita still has another reason to be installed, so pacman keeps it.

That is much safer than maintaining a text file containing a list of packages and later feeding the same list to pacman -R.

One metapackage per project

This becomes particularly useful when developing several applications.

I write more than one GTK application, and they do not necessarily have identical requirements. Rather than creating one enormous “GTK development” package containing everything I might ever use, each project can have its own metapackage.

For example:

text
terminal-dev
├── gtk4
├── libadwaita
├── vte4
└── blueprint-compiler

notes-app-dev
├── gtk4
├── libadwaita
├── gtksourceview5
└── blueprint-compiler

image-app-dev
├── gtk4
├── libadwaita
└── libspelling

Now suppose I finish working on the terminal and remove terminal-dev.

vte4 can disappear if nothing else requires it.

But GTK, libadwaita, and Blueprint Compiler remain because notes-app-dev and image-app-dev still depend on them.

When I eventually remove the notes application environment as well, gtksourceview5 can go, but the packages still required by image-app-dev stay installed.

In other words, overlapping development environments behave exactly as I want them to:

text
                    gtk4
                   ▲    ▲
                  /      \
        terminal-dev    notes-app-dev
             │                │
             └──► libadwaita ◄┘

I do not need to work out which dependencies are shared. Pacman’s dependency resolver already has that information.

Each project describes what it actually needs

There is another useful consequence of keeping separate metapackages: projects are free to evolve independently.

One project might require:

makefile
depends=(
  'gtk4'
  'libadwaita'
  'vte4'
)

while another needs:

makefile
depends=(
  'gtk4'
  'libadwaita'
  'gtksourceview5'
  'libspelling'
  'blueprint-compiler'
)

There is no pressure to make my whole workstation conform to a single manually maintained “development packages” list.

The metapackage becomes a small description of the native environment required to work on that particular project.

If I add a dependency to the project, I add it to the PKGBUILD. If I stop using one, I remove it.

That also means returning to an old project is easier. The dependency list is not something I have to reconstruct from shell history or memory; it is part of the project’s development setup.

Direct and transitive dependencies

I also try to list dependencies according to what the project actually uses rather than reproducing the entire dependency tree.

Suppose libadwaita depends on another package internally. My application does not necessarily need to list that package explicitly.

For example:

text
my-dev-package
    │
    └── libadwaita
            │
            └── some-library

If some-library is purely a dependency of libadwaita, libadwaita should be responsible for declaring it.

This matters because dependency graphs change.

If a future libadwaita release stops using that library, I do not want my development metapackage to keep it installed forever simply because I copied it into my own dependency list.

On the other hand, if my application directly uses GTK, I am happy to list gtk4 explicitly even when another dependency would currently pull it in anyway. It documents an actual requirement of the project rather than an accidental property of today’s dependency graph.

Build tools belong in the environment too

Some dependencies are only required while building the application.

Blueprint Compiler is a good example. A packaged application generally does not need Blueprint Compiler at runtime; its UI files have already been compiled.

In the PKGBUILD for the application itself, such a tool would normally be treated as a build dependency rather than a runtime dependency.

A development-environment metapackage has a different purpose.

Its depends array effectively means:

These are the things I want installed while I am working on this project.

So including tools such as blueprint-compiler as dependencies of the metapackage makes sense. Removing the development environment later removes those tools too, provided nothing else still requires them.

This distinction is useful: the application’s package describes what users of the finished application need, while the development metapackage describes what I need to work on it.

What about packages that were already installed?

There is an important detail in pacman’s package database: it records an installation reason.

A package can have been installed explicitly, or it can have been installed as a dependency.

Suppose I had already deliberately installed GTK before installing my development metapackage. Pacman knows that GTK was explicitly installed.

Installing another package which depends on GTK does not erase that information.

Consequently, removing my metapackage does not unexpectedly undo my previous decision to have GTK installed.

The installation reason can be inspected with:

sh
pacman -Qi gtk4

or packages can be listed according to how they were installed:

sh
pacman -Qe
pacman -Qd

This behaviour is another reason I prefer letting pacman manage the relationships rather than writing my own cleanup script.

Why not just install everything with --asdeps?

Another possible approach is:

sh
sudo pacman -S --asdeps gtk4 libadwaita vte4 blueprint-compiler

This tells pacman that the packages were installed as dependencies rather than explicitly.

That can be useful, but the metapackage gives me something extra: a named reason for those dependencies to exist.

While adw-vte-dev is installed, its requirements are not orphans. They are required by adw-vte-dev.

More importantly, the dependency list is declarative.

Instead of my system merely containing several packages marked “installed as a dependency”, I have a small package that says:

text
These packages collectively make up the development environment
for this project.

That is both easier to understand and easier to maintain.

Why I prefer this approach

The benefits are fairly simple.

Cleanup is safe. I remove the metapackage and let pacman determine which dependencies are no longer required.

Shared dependencies are preserved. If two projects need GTK, removing one project’s environment does not disturb the other.

Projects can have different requirements. Each project gets its own dependency set instead of contributing to one ever-growing global development environment.

The setup is declarative. The PKGBUILD records what the project needs instead of leaving that information buried in my package database or shell history.

Transitive dependencies remain somebody else’s problem. I declare the packages my environment actually needs and let the Arch packages declare their own dependencies.

Development tools can be managed alongside libraries. Compilers, code generators, and similar tools can exist for exactly as long as one or more development environments require them.

And perhaps most importantly, it uses pacman’s existing dependency model rather than inventing a second package-management system on top of it.

A small package with a useful job

A metapackage containing no files can initially look a little pointless.

In this case, though, the package itself is not the interesting part. The dependency relationship is.

The metapackage gives a collection of development dependencies an identity:

text
This is the environment for project X.

Once that relationship exists, pacman can do what a package manager is good at: keep track of why packages are installed, which packages still need them, and which ones can safely disappear.

For project-specific native dependencies on Arch Linux, that turns out to be a very convenient way of keeping a development machine tidy. path: src/data/blog/arch-metapackages-project-dependencies.md