Let’s face it. Packaging software on Linux is an underappreciated job. If they do their job correctly, almost no one will notice them. But the moment they do a mistake, the users of their distribution will likely go mad.
They often work with no standard tool for build and install. They have to dig into other people source code daily and fix errors regarding build and dependencies. Likewise, they also have to discuss and teach developers about how distributions and standard works. Knowing Linux hierarchy, where to place files and a lot of unwritten rules to make systems work is surely out of the scope for most developers. And I think they have all the rights to skip this knowledge. They develop software, they don’t build systems (but there are exceptions too!).
However, developers are (almost) always asked to write the installation target for their software, may it be a shell script or a complex C codebase. Here, tools often don’t help the developers, as they don’t properly support or enforce the various standards. Therefore, package maintainers have to fill the gap themselves. It usually happens in their own distribution package specification. How many distributions are there? Tons. And this work is duplicated countless times.
While developing software written in Rust, I have found a gap in cargo-install
: it doesn’t
consider any data or file outside of executables and libraries. In spite of that, applications
usually have many files to install; man pages, documentation, examples, .desktop
files and icons. What do developers do in this case? They either add a Makefile
or entirely
skip the installation target. This is something that happens not only to Rust projects, but
to many others types.
I have engineered a tool to fill the gap, rinstall, and this post is about its rationale, usage and how it could improve packaging on Linux (and other *nix systems too!).
rinstall
rinstall is a small (at the time of writing, just ~600 LOC!) helper tool that has two main objectives:
- Help developers define which files their program shall install without knowing all the various standards, hierarchy and the quirks of writing Makefiles.
- Remove the duplicated code in the distribution packages definitions that manually use
cp
andinstall
to install files.
It doesn’t try to replace tools such as meson
, that already wonderfully covers the installation
phase. It doesn’t try to replace either CMake
or make
when they are already used for
compiling in C/C++ projects. Although they don’t do any enforcing, they are already used,
so it doesn’t make a lot of sense to add a new dependency on a new tool.
I am sure most of you are ready to throw knives at me the moment I said I want to replace
Makefiles
in some contexts. I know what you’re feeling, but please hold on and continue reading.
There will be an in-depth comparison and explanation. If you still aren’t satisfied by then,
I won’t stop you anymore!
Install directories
rinstall supports two installation modes out of the box: system-wide and user install.
The first installs into /usr/local
by default, making the programs available to all system users.
It follows the Directory Variables from the GNU Coding Standard. Binaries will be installed
into /usr/local/bin
, libraries into /usr/local/lib
and so on. The prefix can be changed by
passing it into the command line, as well as all the other variables.
The second installs into the HOME
folder of the current user, following the
XDG Base Directory Specification. Binaries will be installed into ~/.local/bin
,
libraries into ~/.local/lib
, and all the other variables will use the various XDG_*
variables
with default fallback.
It also reads the configuration from /etc/rinstall.yml
when running as root and
.config/rinstall.yml
when running in user mode. If the configuration file exists,
the tool will merge said config with the default one.
In addition, it also supports a --destdir
argument. Packagers can just call the following
command to perform the installation phase of a program:
# rinstall --destdir mydir --prefix /usr --no-config
install.yml
rinstall resolves around the per-project file install.yml
. This file contains the
list of files to install, divided per category, as well as the name and version of the software.
For example this is the install.yml
for rinstall itself:
name: rinstall
version: 0.1
type: rust
exe:
- src: rinstall
docs:
- src: LICENSE.md
- src: README.md
Each outmost key (exe
and docs
in this case) contains a list of sources, abbreviated src
,
and optionally the destination, abbreviated dst
. rinstall will install the source into either
the folder or file that destination points to, using the respective folder the for key used.
It supports many keys and common cases, allowing to the developers to write
as little as possible. In this case, its output would be:
# rinstall
Installing "target/release/rinstall" to "/usr/local/bin/rinstall"
Installing "LICENSE.md" to "/usr/local/share/doc/rinstall-0.1/LICENSE.md"
Installing "README.md" to "/usr/local/share/doc/rinstall-0.1/README.md"
One of the projects that I stumbled upon which would benefit from using rinstall is the
widely used terminal emulator Alacritty. In addition to its static executable (Rust did it
again!), there is the man page, the example configuration, the .desktop
file and its logo.
However, it lacks a Makefile
and so the packagers (as well as the users) are required to
manually install these files. The Arch Linux alacritty package
contains the following instructions:
desktop-file-install -m 644 --dir "$pkgdir/usr/share/applications/" "extra/linux/Alacritty.desktop"
install -D -m755 "target/release/alacritty" "$pkgdir/usr/bin/alacritty"
install -D -m644 "extra/alacritty.man" "$pkgdir/usr/share/man/man1/alacritty.1"
install -D -m644 "extra/linux/io.alacritty.Alacritty.appdata.xml" "$pkgdir/usr/share/appdata/io.alacritty.Alacritty.appdata.xml"
install -D -m644 "alacritty.yml" "$pkgdir/usr/share/doc/alacritty/example/alacritty.yml"
install -D -m644 "extra/completions/alacritty.bash" "$pkgdir/usr/share/bash-completion/completions/alacritty"
install -D -m644 "extra/completions/_alacritty" "$pkgdir/usr/share/zsh/site-functions/_alacritty"
install -D -m644 "extra/completions/alacritty.fish" "$pkgdir/usr/share/fish/vendor_completions.d/alacritty.fish"
install -D -m644 "extra/logo/alacritty-term.svg" "$pkgdir/usr/share/pixmaps/Alacritty.svg"
Its install.yml
would be:
name: alacritty
version: 0.8.0
type: rust
exe:
- src: alacritty
man:
1:
- extra/alacritty.man
completions:
- bash: extra/completions/alacritty.bash
- fish: extra/completions/alacritty.fish
- zsh: extra/completions/_alacritty
data:
- src: extra/logo/alacritty-term.svg
dst: icons/hicolor/scalable/apps/Alacritty.svg
docs:
- src: alacritty.yml
dst: example/
appdata:
- src: extra/linux/io.alacritty.Alacritty.appdata.xml
desktop-files:
- src: extra/linux/Alacritty.desktop
And this would be the output from rinstall:
# rinstall
Installing "target/release/alacritty" to "/usr/local/bin/alacritty"
Installing "extra/logo/alacritty-term.svg" to "/usr/local/share/icons/hicolor/scalable/apps/Alacritty.svg"
Installing "extra/alacritty.man" to "/usr/local/share/man/man1/alacritty.1"
Installing "alacritty.yml" to "/usr/local/share/doc/alacritty-0.8.0/example/alacritty.yml"
Installing "extra/linux/Alacritty.desktop" to "/usr/local/share/applications/Alacritty.desktop"
Installing "extra/linux/io.alacritty.Alacritty.appdata.xml" to "/usr/local/share/appdata/io.alacritty.Alacritty.appdata.xml"
Installing "extra/completions/alacritty.bash" to "/usr/local/share/bash-completion/completions/alacritty.bash"
Installing "extra/completions/alacritty.fish" to "/usr/local/share/usr/share/fish/vendor_completions.d/alacritty.fish"
Installing "extra/completions/_alacritty" to "/usr/local/share/zsh/site-functions/_alacritty"
The original Arch Linux package contains 9 hardcoded install instructions that could be replaced by 1 single line of a call to rinstall. That’s 8 lines less. According to repology, alacritty is packaged for more than 30 different distributions. That’s 240 lines less gained by improving a single package! Yea, the improvement might actually be minor for other packages, but it’s still considerable, counting the thousand of packages out there!
Makefiles comparison
I am sure many are asking why not add a Makefile
for Alacritty instead? That’s a great question.
In my opinion, these are the various reasons:
- its syntax is complicated and not easy to learn. Learning it as a requirement to install a simple shell script seems overkill to me.
- it is made to track dependencies between sources and objects and correctly compile software.
- not only Directory Variables standard is not enforced, the developer has to write support for it by himself. Asking the developer to learn an entire standard and the quirks of the Filesystem Hierarchy Standard seems again overkill.
While I was discussing rinstall with a developer, he sent me this
Makefile
to show me that they are really simple to write and that they support MANDIR
and DOCDIR
without issues. Let’s review its content:
PREFIX ?= /usr
MANDIR ?= $(PREFIX)/share/man
DOCDIR ?= $(PREFIX)/share/doc/fff
all:
@echo Run \'make install\' to install fff.
install:
@mkdir -p $(DESTDIR)$(PREFIX)/bin
@mkdir -p $(DESTDIR)$(MANDIR)/man1
@mkdir -p $(DESTDIR)$(DOCDIR)
@cp -p fff $(DESTDIR)$(PREFIX)/bin/fff
@cp -p fff.1 $(DESTDIR)$(MANDIR)/man1
@cp -p README.md $(DESTDIR)$(DOCDIR)
@chmod 755 $(DESTDIR)$(PREFIX)/bin/fff
uninstall:
@rm -rf $(DESTDIR)$(PREFIX)/bin/fff
@rm -rf $(DESTDIR)$(MANDIR)/man1/fff.1
@rm -rf $(DESTDIR)$(DOCDIR)
But, ironically, he sent me a Makefile with an error in it. Did you spot it?
I am sure that somebody did. For the rest, don’t mind, it’s actually a little (but crucial!)
detail: the default PREFIX
is /usr
instead of /usr/local
. Not only this is wrong
according to the standard, but it opens the user to subtle breakage. The user could run make install
as root and expect to have the newly installed files into /usr/local
. He would then
to discover afterwards that it has silently installed into /usr
. Only the package manager
should install into /usr
, as it tracks all the files there. He may have
overwritten packages installed by the package manager and have potentially broken the system!
The equivalent install.yml
is:
name: fff
version: 0.1
type: custom
exe:
- src: fff
man:
1:
fff.1
docs:
- src: README.md
This was pretty a pretty subtle error to spot. More often the error will be easier to catch but
just as bad, such as a missing DESTDIR
. I think it’s not the developers mistake, as it is not fair
to ask them to write such fragile files following a standard that they might not even know that
well.
Source based distributions
rinstall has been written in Rust. It is a language I have been learning recently and that I fun to write into. It is also safe and fast, so I didn’t give a second thought to use it here. However, there have been an excellent critic regarding rinstall by a Gentoo maintainer: does it mean that Rust toolchain will be needed by Gentoo stage3 when performing the base installation?
rinstall aims at projects that are not already using a build system capable of installing
software. In other words, I don’t want to replace Makefiles when they are already used
for compiling sources. This already exclude most of the software that needs to be compiled
in a stage3 or when bootstrapping. Moreover, having an install.yml
does not enforce anyone to use
rinstall; the files could still be installed manually. In alternative, there will be a
release of rinstall available for anyone to grab; this will allow users to just download
the static executable of rinstall and use it.
Conclusion
This might as well be as introducing a new “standard” way to install files into the system. However, I feel like an improvement in that regard is possible and that adding a new standard is the only way to do so.
rinstall has been written in just a week and is actually missing some interesting use cases, such as installing software from the release tarball. This is something that I am looking forward to adding in the near future.
I want to thank everyone that has provided feedback on both rinstall and this post.