Havoc Pennington
hp@pobox.com 2002
Philip Withnall
philip.withnall@collabora.co.uk 2015

This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.

Als besondere Ausnahme gewähren die Inhaber des Urheberrechts die Erlaubnis, den Beispiel-Code in diesem Dokument ohne Einschränkungen unter den Bedingungen Ihrer Wahl zu kopieren, zu verändern und weiterzugeben.

Writing libraries to be future proof through parallel installation
Mario Blättermann
mario.blaettermann@gmail.com 2016, 2018
Christian Kirbach
christian.kirbach@gmail.com 2016, 2020
Stefan Melmuk
stefan.melmuk@gmail.com 2018
Tim Sabsch
tim@sabsch.com 2021

# Parallele Installierbarkeit

#

        Zusammenfassung

        Wenn zwei Pakete parallel installiert werden, haben Sie keine gemeinsamen Dateinamen, und Entwickler, die auf dem Paket aufbauen, kompilieren stets gegen die erwartete Version. Dies gilt auch für Daemons, Dienstprogramme und Konfigurationsdateien sowie Header-Dateien und Bibliothek-Binärdateien.

          Stellen Sie sicher, dass alle Versionen einer Bibliothek parallel installierbar sind. ()
          Versionieren Sie alle Dateien, die von einer Bibliothek installiert werden. ()
          Definieren Sie die Paketversionsnummer separat vom Sonamen oder der libtool-Versionsnummer. Seien Sie sich bewusst, welcher Teil der Paketversionsnummer sich mit der API ändert. ()
          Installieren Sie C-Header-Dateien in $(includedir)/libBibliothek-Version/Bibliothek/. ()
          Installieren Sie Bibliothek-Binärdateien in $(libdir)/liblibrary-version.so.soname. ()
          Installieren Sie pkg-config-Dateien in $(libdir)/pkgconfig/Bibliothek-Version.pc. ()
          Sorgen Sie dafür, dass Konfigurationsdateien aufwärts- und abwärtskompatibel sind oder installieren Sie sie in $(sysconfdir)/Bibliothek-Version/. ()
          Setzen Sie GETTEXT_PACKAGE auf Bibliothek-Version. ()
          Schließen Sie eine Versionsnummer in alle D-Bus-Schnittstellennamen, Dienstnamen und Objektpfade ein. Zum Beispiel: org.domain.LibraryVersion.Interface, org.domain.LibraryVersion und /org/domain/LibraryVersion/. ()
          Installieren Sie Daemon-Binaries in $(libexecdir)/Bibliothek-daemon-Version. ()
          Installieren Sie Hilfs-Binärdateien in $(bindir)/library-utility-version und platzieren Sie symbolische Links in $(bindir)/library-utility. ()

# Begründung

Alle öffentlichen Bibliotheken sollten so entworfen sein, dass sie parallel installiert werden können, damit eventuelle spätere API-Inkompatibilitäten leichter abgefangen werden können. Wenn eine Bibliothek von mehreren Projekten verwendet wird und eine inkompatible Änderung an der API einführt, müssen alle Projekte zeitgleich auf die neue API migrieren, da sie sonst aufgrund der unterschiedlichen Versionsabhängigkeiten nicht länger gemeinsam installierbar sind.

Das ist aus Betriebssicht nicht vertretbar. Außerdem ist die Anforderung, dass alle Projekte unmittelbar auf die neue API portieren müssen, schwer zu organisieren und dürfte zu Unmut führen, da die meisten API-Änderungen keinen großen neuen Funktionen einführen, die eine Portierung motivieren würden.

Die Lösung ist daher, dass alle Bibliotheken parallel installierbar sein müssen: alte und neue Versionen der API müssen ohne Konflikte gleichzeitig installiert und gegeneinander kompiliert sein. Es ist deutlich einfacher, dieses System von paralleler Installierbarkeit von Beginn an zu unterstützen, als es später nachträglich einzuführen.

Es eliminiert das »Henne und Ei«-Problem, eine Reihe an Anwendungen von einer Bibliotheksversion auf die nächste zu portieren und erlaubt es den Maintainern von Bibliotheken, API-Änderungen einfacher einzuführen, wodurch schlussendlich schnellere Iterationen und Entwicklungsprozesse ermöglicht werden.

Eine ebenfalls valide Alternative hierzu ist, dass die Bibliothek niemals API-Inkompatibilitäten einführt. Dieser Ansatz wird von `libc` verfolgt.

# Lösung

Die Lösung für dieses Problem besteht im Prinzip darin, die Bibliothek umzubenennen. In den meisten Fällen ist es hier am einfachsten, die Versionsnummer in den Pfad jeder installierten Datei einzubinden. Dadurch können mehrere Versionen der Bibliothek gleichzeitig installiert sein.

Nehmen wir an, die Bibliothek `Foo` installiert traditionell diese Dateien:

/usr/include/foo.h

/usr/include/foo-utils.h

/usr/lib/libfoo.so

/usr/lib/pkgconfig/foo.pc

/usr/share/doc/foo/foo-manual.txt

/usr/bin/foo-utility

Nun können Sie Version 4 von `Foo` so anpassen, dass sie stattdessen diese Dateien installiert:

/usr/include/foo-4/foo/foo.h

/usr/include/foo-4/foo/utils.h

/usr/lib/libfoo-4.so

/usr/lib/pkgconfig/foo-4.pc

/usr/share/doc/foo-4/foo-manual.txt

/usr/bin/foo-utility-4

Es kann dann parallel mit Version 5 installiert werden:

/usr/include/foo-5/foo/foo.h

/usr/include/foo-5/foo/utils.h

/usr/lib/libfoo-5.so

/usr/lib/pkgconfig/foo-5.pc

/usr/share/doc/foo-5/foo-manual.txt

/usr/bin/foo-utility-5

Sie können dieses Verhalten mit pkg-config umsetzen: foo-4.pc würde /usr/include/foo-4 in den include-Pfad und libfoo-4.so zu der Bibliothekenliste hinzufügen; foo-5.pc würde /usr/include/foo-5 und libfoo-5.so hinzufügen.

# Versionsnummern

Die Versionsnummer in den Dateipfaden ist eine ABI/API-Version. Es sollte sich nicht um die vollständige Versionsnummer Ihres Pakets handeln, sondern nur um den Teil, der die API-Kompatibilität angibt. Wenn Sie das Standard-Schema `major.minor.micro` für Ihre Projektversionierung verwenden, wird die API-Version typischerweise durch die Major-Versionsnummer angegeben.

Minor-Veröffentlichungen (bei denen für gewöhnlich APIs hinzugefügt, aber nicht verändert oder entfernt werden) sowie Micro-Veröffentlichungen (für gewöhnlich Fehlerbehebungen) betreffen nicht die API-Abwärtskompatibilität und erfordern daher nicht, dass alle Dateien verschoben werden.

Die Beispiele in den nachfolgenden Abschnitten gehen davon aus, dass die API-Version und der Soname mit dem folgenden Code aus der configure.ac entnommen werden:

## API-Versionierung in Autoconf

Code zum Exportieren der API-Version und soname aus configure.ac
# Before making a release, the LIBRARY_LT_VERSION string should be modified. # The string is of the form c:r:a. Follow these instructions sequentially: # # 1. If the library source code has changed at all since the last update, # then increment revision (‘c:r:a’ becomes ‘c:r+1:a’). # 2. If any interfaces have been added, removed, or changed since the last update, # increment current, and set revision to 0. # 3. If any interfaces have been added since the last public release, # then increment age. # 4. If any interfaces have been removed or changed since the last public release, # then set age to 0. AC_SUBST([LIBRARY_LT_VERSION],[1:0:0])

AC_SUBST([LIBRARY_API_VERSION],[4])

# C-Headerdateien

Header-Dateien sollten immer in einen versionierten Unterordner installiert werden, der eine -I-Flag an den C-Compiler erfordert. Wenn zum Beispiel Ihr Header foo.h heißt und Ihre Anwendungen Folgendes tun:

#include <foo/foo.h>

dann sollten Sie diese Dateien installieren:

/usr/include/foo-4/foo/foo.h

/usr/include/foo-5/foo/foo.h

Anwendungen sollten die Flags -I/usr/include/foo-4 oder -I/usr/include/foo-5 an den Compiler übergeben. Zur Erinnerung: dies wird mittels pkg-config realisiert.

Beachten Sie den zusätzlichen Unterordner foo/. Dieser legt den Namensraum `#include` an, um Dateinamen-Konflikte mit anderen Bibliotheken zu vermeiden. Wenn zum Beispiel zwei verschiedene Bibliotheken Header namens utils.h installieren: welcher wird verwendet, wenn Sie `#include <utils.h>` schreiben?

Es klingt verlockend, eine der Header-Dateien außerhalb eines Unterordners zu platzieren:

/usr/include/foo.h

/usr/include/foo-5/foo.h

Das Problem hierbei ist aber, dass Nutzer immer aus Versehen den falschen Header abbekommen, da -I/usr/include immer mal wieder seinen Weg in die Kompilierungsbefehle findet. Wenn Sie Ihre Header-Dateien dennoch unbedingt so platzieren müssen, sollten Sie in Ihrer Bibliothek wenigstens einen Check hinzufügen, der überprüft, ob Anwendungen bei der Initialisierung der Bibliothek den falschen Header verwenden.

Versionierte Header-Dateien können in automake über den folgenden Code installiert werden:

## Header-Dateien in Automake

Code, um versionierte Header-Dateien über Makefile.am zu installieren
library
includedir = $(includedir)/liblibrary-@LIBRARY_API_VERSION@/library library_headers = \ liblibrary/example1.h \ liblibrary/example2.h \ $(NULL)

# The following headers are private, and shouldn't be installed: private_headers = \ liblibrary/example-private.h \ $(NULL) # The main header simply #includes all other public headers: main_header = liblibrary/library.h public_headers = \ $(main_header) \ $(library_headers) \ $(NULL)

library
include_HEADERS = $(public_headers)

Zusätzlich zur korrekten Versionierung sollten alle APIs in installierten Header-Dateien die richtigen Namensräume besitzen.

# Bibliotheken

Bibliothek-Objektdateien sollten einen versionierten Namen besitzen. Zum Beispiel:

/usr/lib/libfoo-4.so

/usr/lib/libfoo-5.so

Dies erlaubt Anwendungen, die richtige Version zur Kompilierungszeit zu erhalten und stellt sicher, dass die Versionen 4 und 5 keine gemeinsamen Namen besitzen.

Versionierte Bibliotheken können in automake über den folgenden Code gebaut und installiert werden:

## Bibliotheken in Automake

Code, um versionierte Bibliotheken über Makefile.am zu bauen und installieren
lib_LTLIBRARIES = liblibrary/liblibrary-@LIBRARY_API_VERSION@.la

liblibrary_liblibrary_@LIBRARY_API_VERSION@_la_SOURCES = \ $(private_headers) \ $(library_sources) \ $(NULL) liblibrary_liblibrary_@LIBRARY_API_VERSION@_la_CPPFLAGS = … liblibrary_liblibrary_@LIBRARY_API_VERSION@_la_CFLAGS = … liblibrary_liblibrary_@LIBRARY_API_VERSION@_la_LIBADD = … liblibrary_liblibrary_@LIBRARY_API_VERSION@_la_LDFLAGS = \ -version-info $(LIBRARY_LT_VERSION) \ $(AM_LDFLAGS) \ $(NULL)

## Bibliothek-Sonamen

Bibliothek-Sonamen (auch als libtool-Versionsnummern bekannt) adressieren nur das Problem, dass bereits früher kompilierte Versionen zu Anwendungen verlinkt werden. Sie adressieren nicht das Problem, wenn kompilierende Anwendungen vorherige Versionen benötigen, und betreffen ausschließlich Bibliotheken.

Aus diesem Grund sollten Sonamen zwar verwendet werden, allerdings zusätzlich zu versionierten Bibliotheksnamen. Beide Lösungen gehen unterschiedliche Probleme an.

# pkg-config-Dateien

pkg-config-Dateien sollten einen versionierten Namen besitzen. Zum Beispiel:

/usr/lib/pkgconfig/foo-4.pc

/usr/lib/pkgconfig/foo-5.pc

Da jede pkg-config-Datei versionierte Informationen über den Bibliotheksnamen und die include-Pfade enthält, sollte jedes Projekt, das von der Bibliothek abhängt, von einer Version auf die nächste wechseln können, indem sie einfach ihren pkg-config-Check von foo-4 auf foo-5 ändert (und eventuelle API-Änderungen portiert).

Versionierte pkg-config-Dateien können über Autoconf und Automake mit dem folgenden Code installiert werden:

## pkg-config-Dateien in Autoconf und Automake

Code, um versionierte pkg-config-Dateien über configure.ac und Makefile.am zu installieren
AC_CONFIG_FILES([ liblibrary/library-$LIBRARY_API_VERSION.pc:liblibrary/library.pc.in ],[], [LIBRARY_API_VERSION='$LIBRARY_API_VERSION'])
# Note that the template file is called library.pc.in, but generates a # versioned .pc file using some magic in AC_CONFIG_FILES. pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = liblibrary/library-$(LIBRARY_API_VERSION).pc

DISTCLEANFILES += $(pkgconfig_DATA) EXTRA_DIST += liblibrary/library.pc.in

# Konfigurationsdateien

Für den Nutzer ist der beste Ansatz für Konfigurationsdateien, ihr Format aufwärts- und abwärtskompatibel zu halten (beide Bibliotheksversionen erlauben die genau gleiche Konfigurationsdatei-Syntax und -Semantik). Damit kann die gleiche Konfigurationsdatei für alle Versionen der Bibliothek verwendet werden, und die Konfigurationsdatei selbst muss nicht versioniert werden.

Falls das nicht möglich ist, sollten die Konfigurationsdateien schlicht umbenannt werden, und Nutzer müssen jede Version separat konfigurieren.

# Gettext-Übersetzungen

Wenn Sie für Übersetzungen gettext im Kombination mit autoconf und automake verwenden, werden Übersetzungen für gewöhnlich in /usr/share/locale/lang/LC_MESSAGES/package installiert. Sie müssen dabei package ändern. Die GNOME-Konvention ist, dafür configure.ac zu verwenden:

GETTEXT_PACKAGE=foo-4 AC_SUBST([GETTEXT_PACKAGE]) AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE],["$GETTEXT_PACKAGE"])

Verwenden Sie `GETTEXT_PACKAGE` als den Paketnamen, der an `bindtextdomain()`, `textdomain()` und `dgettext()` übergeben wird.

# D-Bus-Schnittstellen

Eine D-Bus-Schnittstelle ist eine andere Form von API, ähnlich zu einer C-API mit der Ausnahme, dass die Versionsauflösung zur Laufzeit statt beim Kompilieren stattfindet. Abgesehen davon werden D-Bus-Schnittstellen genauso wie C-APIs versioniert: Versionsnummern müssen in den Namen der Schnittstelle und Diensten sowie in den Objektpfaden enthalten sein.

Für einen Dienst `org.example.Foo`, der die Schnittstellen `A` und `B` auf den Objekten `Controller` und `Client` bereitstellt, würden die Versionen 4 und 5 der D-Bus-API wie folgt aussehen:

## Dienstnamen

`org.example.Foo4`

`org.example.Foo5`

## Schnittstellennamen

`org.example.Foo4.InterfaceA`

`org.example.Foo4.InterfaceB`

`org.example.Foo5.InterfaceA`

`org.example.Foo5.InterfaceB`

## Objektpfade

`/org/example/Foo4/Controller`

`/org/example/Foo4/Client`

`/org/example/Foo5/Controller`

`/org/example/Foo5/Client`

# Programme, Daemons und Dienstprogramme

Desktop-Anwendungen müssen für gewöhnlich nicht versioniert werden, da keine anderen Module von ihnen abhängen. Daemons und Dienstprogramme interagieren hingegen mit anderen Teilen des Systems und erfordern daher eine Versionierung.

Es sei ein Daemon und ein Dienstprogramm vorhanden:

/usr/libexec/foo-daemon

/usr/bin/foo-lookup-utility

diese sollten wie folgt versioniert werden:

/usr/libexec/foo-daemon-4

/usr/bin/foo-lookup-utility-4

Vermutlich möchten Sie einen symbolischen Link von /usr/bin/foo-lookup-utility zur empfohlenen versionierten Kopie des Dienstes setzen, um Nutzern die Verwendung zu vereinfachen.