Softwarebau ohne Build System

Dieser Artikel ist Bestandteil der Serie Build Systeme, in welcher verschiedene Systeme zum Softwarebau gegenüber gestellt werden. Um zum übergeordneten Artikel zu gelangen, klicken Sie bitte hier.

Das Erbe von Unix

Im Gegensatz zu den eher anwenderorientierten Betriebssystemen der fortgeschrittenen 80er gilt es unter den eher traditionellen Betriebssysement wie Unix und seinen Abkömmlingen als Usus, neben dem eigentlichen System passende Entwicklungswerkzeuge zur Verfügung zu stellen. Dies rührt daher, dass insbesondere in der Frühzeit der Informatik der Einsatzzweck eines Computers hauptsächlich in seiner Programmierung lag. Im Gegensatz zu heute waren vielfältige Programme zur Lösung aller erdenklichen Aufgaben nicht verfügbar und so war die Möglichkeit eigene Programme zu erstellen eine der wichtigsten Voraussetzungen, um einen Computer überhaupt betrieben zu können. So ist es kein Wunder, dass nicht erst seit THE EPOCH Compiler und entsprechende Entwicklungswerkzeuge zum Standardlieferumfang eines guten Betriebssytems gehören.

Aufgrund der engen, historischen Bindung von Unix an die Programmiersprache C findet sich auf sogut wie jedem Unixsystem auch ein C-Compiler, der ganz im Sinne der Unix-Tradition mit dem Kommando cc aufgerufen wird. Er nimmt eine Reihe von Quelldateien (*.c und *.h) und wandelt sie in binäre Objektdateien um. Dabei handelt es sich um fast lauffähigen Maschinencode, dem zur Ausführung aber noch wesentliche Teile fehlen. Diese Teile zu ergänzen ist die Aufgabe des Linkers, der seinen Namen aus der Tatsache ableitet, dass er mehrere Objektdateien zusammenknüpfen kann, um daraus ein ausführbares Programm zu erstellen. Doch der Linker kann noch mehr, als nur Objektdateien verknüpfen. Seine wahre Stärke liegt darin, dass er aus den Objektdateien auch Bibliotheken erstellen kann, gegen die später entwender statisch oder dynamisch gelinkt werden kann.

Der Unterschied zwischen statischen und dynamischen Bibliotheken ist im Prinzip gering, wirkt sich aber entscheidend auf den Bindevorgang aus. Statische Bilbiotheken werden in der Regel positionsabhängig - also nicht reallokierbar - compiliert. Das heißt, sie verwenden feste Speicheradressen innerhalb des virtuellen Adressraums einer Anwendung und müssen darum an einer festen Stelle in den Speicher geladen werden. Um dies sich zu gewährleisten, werden statische Bibliotheken immer an das ie verwendende Programm angehängt. Sie sind somit untrennbarer Bestandteil des fertigen Programms und müssen nicht separat dazu ausgeliefert werden. Dynamische Bibliotheken werden im Gegnsatz dazu immer reallokierbar compiliert und während dem Binden nicht an das referenzierende Programm angehängt. Hierfür muss das Betriebssystem, auf dem das Prorgramm laufen soll, einen speziellen Lademechanismus bereitstellen, der vor der Programmausführung die benötigten Bibliotheken in den Speicher lädt und mit Hilfe des Linkers mit dem Maschinencode der Anwendung verbindet. Dieses Vorgehen birgt im Gegensatz zum statischen Verfahren den Vorteil, dass die ausführbaren Dateien kleiner sind und redundanter Code zwischen verschiedenen Anwendungen vermieden wird. Der Preis dafür ist eine geringfügige Verzögerung beim Programmstart sowie die Einführung von Abhängigkeiten zu installierten Bibliotheken auch während der Laufzeit. Eine Bibliothek muss also nicht mehr nur installiert sein, um das Programm bauen zu können, sie muss auch beim Anwender installiert sein, um das Programm benutzen zu können. Zumindest die Benutzer von Microsoft Windows kennen dieses Problem unter dem Namen DLL-Hölle.

Eine Quelldatei compilieren und linken, echte Handarbeit

Wie bereits oben ausgeführt, sind zwei Schritte notwendig, um eine C-Quellendatei in ein ausführbares Binärprogramm umzuwnadeln: Zunächst muss es compiliert und anschließend gelinkt werden. Hierfür stellt Unix traditionell die Kommandos cc und ld zur Verfügung. Bereits für das Beispielprogramm single_file.c sind also mehrere Aufrufe notwendig.

$ cc -c single_file.c

Hierdurch entsteht eine Objektdatei namens single_file.o. Alternativ kann durch das -o Flag ein abweichender Dateiname angegeben werden.

Der Bindevorgang gestalltet sich komplexer, weil hier mehrere Parameter angegeben werden müssen. Ein einfacher Aufruf von

$ ld single_file.o

bringt lediglich eine ganze Menge von Fehlermeldungen aber nicht den erhofften Erfolg. Das liegt zunächst daran, dass der Linker die Verweise auf libcaca nicht auflösen kann, die vom Testprogramm aber verwendet wird. Damit dies funktioniert, braucht der Linker weiteren Input. Es muss dabei allerdings berücksichtigt werden, dass libcaca selbst auch eine Abhängigkeit besitzt, nämlich zu libcucul. Diese muss dem Linker ebenfalls bekannt gemacht werden. Somit sieht ein Aufruf von

$ ld single_file.o -lcaca -lcucul

(-lXXX teilt dem Linker mit, dass gegen eine Bibliothek XXX gebunden werden soll, wobei die Reihenfolge ausschlaggebend für den Bindevorgang ist) schon besser aus, bringt aber immer noch eine Warnung. Zwar entsteht ein ausführbares Programm, es kann aber nicht gestartet werden. Warum, das besagt die Warnung mit den treffenden Worten:

ld: warning: cannot find entry symbol _start; defaulting to 0000000000400800

Der Linker findet also den Einstiegspunkt in das Programm nicht und geht deshalb von einem Standardwert aus. Offenbar ist dies aber nicht der Wert, von dem der C-Compiler ausging. Ein kurzer Blick in die man-page von ld gibt Aufschluss: Der Parameter -e bzw. --entry dient dazu, dieses Manko zu beheben und dem Linker einen Symbolnamen oder eine Adresse als Einstiegspunkt mitzugeben. Dieser ist in C-Programmen üblichweiße durch die main-Funktion gegeben. Somit fruchtet der folgende Aufruf von ld endlich:

$ ld single_file.o -lcaca -lcucul -e main

Dabei entsteht eine Binärdatei namens single_file, die sich problemlos ausführen lässt. Fassen wir also zusammen. Die benötigten Aufrufe, um aus nur einer Quelldatei namens single_file.c, welche sich auf eine externe Bibliothek (libcaca0) stützt, ein ausführbares Programm zu erstellen, lauten wie folgt:

$ cc -c single_file.c
$ ld single_file.o -lcaca -lcucul -e main

Es lässt sich leicht absehen, dass daraus schnell eine große Menge an korrekt einzugebenden Befehlen wird, sobald ein Programm aus mehreren Teilen wie verschiedenen Bibliotheken und ausführbaren Dateien besteht. Und das ist gerade mal die Spitze des Kommandozeilenparametereisbergs. Natürlich muss eingeräumt werden, dass die meisten der vorhandenen Parameter nicht alltäglich gebraucht werden. Dennoch wäre es schön, wenn sich der ganze Vorgang einfacher erledigen liese. Zum Glück ist dem auch so, zumindest wenn die GNU Compiler Collection installiert ist. Dann nämlich lassen sich die obigen Aufrufe durch nur einen substituieren:

$ gcc single_file.c -lcaca -lcucul -o single_file

Hier ist die Angabe des -o Parameter sinnvoll, da sonst die Ausgabedatei aus Gründen der Kompatibilität a.out heißt. Im Übrigen ist es so, dass der GNU-Compiler nicht zwischen gcc und cc unterscheidet. Sofern also kein weiterer C-Compiler installiert ist, ist cc ein indirekter, symbolischer Link auf gcc. Im folgenden Teil der Artikel über Build Systeme sollen die verkürzten Aufrufe von gcc genutzt werden, da die GNU Compiler auf vielen Plattformen als Standardcompiler gelten und in der Regel auch für die übrigen Betriebssysteme zur Verfügung stehen.

Ein Wort zu externen Abhängigkeiten

Bereits die Verwendung einer kleinen, externen Bibliothek mit nur wenigen, weiteren Abhängigkeiten verdeutlicht, wie der Einsatz von Bibliotheken schnell zum Problem werden kann. Spätestens beim Binden des Programms müssen alle einzubindenden Bibliotheken bekannt sein, selbst wenn sie im Quellcode des Programms eigentlich nicht verwendet werden. Hat man dieses Problem einmal gemeistert, kann man Glück haben und die Abhängigkeiten ändern sich nicht. Mit der voranschreitenden Weiterentwicklung der verwendeten Bibliotheken und auch des eigenen Programms ist es jedoch wahrscheinlicher, dass sich auch die Abhängigkeiten während des Bindevorgangs ändern. Um also das Problem der DLL-Hölle während der Entwicklung zumindest ein Stück weit zu entschärfen, gibt es unter Linux das Programm pkg-config.

Dieses Programm ermöglicht es zu prüfen, ob eine benötige Bibliothek in einer zulässigen Version überhaupt vorliegt und falls ja, auch die entsprechenden Compiler- und Linkerparameter zu erzeugen. Hierfür bedient es sich spezieller Metadateien, welche von den installierten Bibliotheken zur Verfügung gestellt werden und eine Bibliothek möglichst gut beschreiben.

Typische Aufrufe von pkg-config sind:

pkg-config --exists cacaPrüfen, ob libcaca installiert ist
pkg-config --exact-version=0.1 caca
Prüfen auf Version 0.1 von libcaca
pkg-config --atleast-version=2 --max-version=2.99 gtk
Prüfen, ob die Version von libgtk zwischen 2 und 2.99 liegt
pkg-config --cflags --libs caca
Ausgeben der Compilerparameter (--cflags) und Linkerparameter (--libs), um gegen libcaca zu binden

Gepaart mit der Möglichkeit einer jeden POSIX-Shell, die Ausgabe eines Programms wie eine Benutzereingabe zu interpretieren, lassen sich die eben beschriebenen Probleme beim Einsatz externer Bibliotheken deutlich reduzieren. Hier genügt es schon, den obigen Aufruf von gcc wie folgt abzuändern:

$ gcc single_file.c -o single_file `pkg-config --cflags --libs caca`

oder

$ gcc single_file.c -o single_file $(pkg-config --cflags --libs caca)

was aber nur eine andere Syntax für die Backticks der ersten Zeile ist. Zunächst erscheint das einzugebende Kommando länger, dafür muss man nicht wissen, auf welche Bibliotheken sich libcaca0 stützt. Dies erledigt pkg-config vollautomatisch, indem es die entsprechenden Kommandozeilenparameter generiert. In diesem speziellen Fall könnte sogar auf die Angabe von --cflags verzichtet werden, um die Zeilenlänge etwas zu kürzen, da libcaca0 keine speziellen Parameter für den Compiler benötig. Auf der anderen Seite ist die Sicherheit gegenüber künfitgen Erweiterungen größer, wenn man den Parameter mitführt.

Alles woran jetzt noch gedacht werden muss, ist weitere Bibliotheksnamen an pkg-config zu übergeben, sofern diese im Quelltext verwendet werden.

Die Qual der Wahl oder wie bindet man große Projekte?

Eine ziemlich saloppe Art der Programmmodularisierung besteht darin, häufig benötigte Teile (Funktionen, Klassen, Strukturen, ...) in eigene Quelldateien auszulagern und diese dann bei Bedarf via #include dort einzubinden, wo sie wieder benötigt werden. Dies entspricht im Grunde genommen fast der Modularisierungstechnik, wie sie in C/C++ tatsächlich angewandt wird, umgeht aber die Erstellung einer Headerdatei, in welcher die ausgelagerten Inhalte deklariert werden. Im Gegensatz zur Verwendung von Headerdateien reduziert sich dieser Fall allerdings auf den zuvor dargelegten Sachverhalt, dass ein Programm aus nur einer Quelldatei besteht. Denn noch bevor der Compiler dazu käme, mit der Assemblierung zu beginnen, würde der Präprozessor die verschiedenen Quelldateien wieder zu einer großen, monolithischen Datei zusammenführen. Macht man sich hingegen die Mühe, und erstellt zu jeder ausgelagerten Quelldatei mindestens eine passende Headerdatei, ergibt sich eine Reihe von Vorteilen: Die ausgelagerten Programmteile können zu Bibliotheken gebunden werden und zur Entwicklung von Drittprogrammen müssen nicht die vollen Quellcodes der Bibliothken installiert werden. Es reichen die verhältnismäßig schlanken Headerdateien und vorkompilierte Binärversionen. Dies spart einerseits Speicherplatz und erlaubt andererseits auch die Erstellung von Bibliotheken mit restriktiven Lizenzen ohne Weitergabe des Quellcodes. (Obwohl letzteres für einen echten Verteigier freier Software sicher nur von wenig Belang ist ...)

Diesen Vorteilen steht jedoch die Qual der Wahl gegenüber, was aber nicht als Nachteil empfunden werden darf: Soll gegen eine Bibliothek statisch gelinkt werden, so dass die Teil der Binärdatei wird, oder soll statisch gebunden werden? Beide Verfahren bieten sowohl Vor- als auch Nachteile zum Beispiel hinsichtlich der eigenen Lizenzgestaltung, dem Installations- und Wartungsaufwand beim Endanwender, der Laufzeit und des Speicherplatzbedarfs. Somit ist die richtige Wahl immer fallbezogen, so dass es keine allgemeingültige Aussage gibt. DIe Aussage, die aber getroffen werden kann, ist die dass dynamische Bibliotheken ein eher modernes Konzept sind, dass es in älteren Betriebssystemen (bis in die 80er hinein) noch nicht gab. Heutige Betriebssysteme machen, sofern die nötigen Ressourcen vorhanden sind, jedoch regen Gebrauch davon.

Fällt die Entscheidung auf statisches Binden, steht wieder eine Entscheidung an: Genügt es, mehrere Objektdateien miteinander zu verbinden, oder verdelt man die Objektdateien zuvor zu echten Bibliotheksdateien? Die erste Methode ist sicher adäquat, wenn nur ein Compiler zum Einsatz kommt. Der zweite Weg hat den Vorteil, dass er neben den Anforderungen eines Compilers auch den vom Betriebssystem gestellten Anforderungen entspricht und somit compilerübergreifend funktioniert.

Bezogen auf das zweite Testprogramm gestaltet sich das Zusammenbinden mehrerer Objektdateien einfach. Zunächst werden sie erzeugt,

$ gcc with_libs.c -c
$ gcc msg1.c -c $(pkg-config --cflags caca)
$ gcc msg2.c -c $(pkg-config --cflags caca)

anschließend können sie zusammengebunden werden. Man beachte, dass die Datei with_libs.c die beiden Dateien msg1.c und msg2.c nicht inkludiert. Die #include-Statements beziehen sich stattdessen auf die beiden Headerdateien msg1.h und msg2.h, welche zwar den öffentlichen Inhalt der beiden anderen Quellen liefern, jedoch ohne Implementierung. Die Implementierung wird erst beim Linken in Form der Objektdateien nachgereicht. Es könnte sich dabei auch um fremde Dateien eines anderen Projekts handeln:

$ gcc msg1.o msg2.o with_libs.o -o with_libs $(pkg-config --libs caca)

Das genügt schon, um eine lauffähige Binärdatei zu erzeugen. Dabei ist die doppelte Verwendung von pkg-config eine besondere Erwähnung wert. Da nun das Compilieren getrennt vom Binden ausgeführt wird (das -c Flag verhindert, dass gcc nach dem Compilieren den Linker aufruft, bestehen die Eingabedateien aus Binärdateien, weiß gcc, dass gelinkt werden soll), wird in beiden Schritten pkg-config auf unterschiedliche Weiße benutzt, um die für libcaca0 eventuell benötigten Zusatzparameter zu erzeugen. Im ersten Schritt werden dabei Flags für den C-Compiler erzeuge (--cflags), im zweiten Schritt für den Linker (--libs).

Ähnlich verhält es sich, wenn aus den Objektdateien echte, auslieferbare Bibliothekten gemacht werden sollen. Hierzu müssen ebenfalls erst die Objektdateien generiert werden:

$ gcc msg1.c -c $(pkg-config --cflags caca)
$ gcc msg2.c -c $(pkg-config --cflags caca)

Anschließend können sie zu Archiven, als Bibliotheksdateien, umgewandelt werden:

$ ar cru msg1.a msg1.o
$ ranlib msg1.a

$ ar cru msg2.a msg2.o
$ ranlib msg2.a

Dabei ist der Aufruf von ranlib heutezutage weitestgehendst optional, wird aber von manchen Unices tatsächlich noch benötigt. Schaden tut es auf keinen Fall. Anschließend kann gegen die statischen Bibliotheken gelinkt werden. Hierzu ist keine Sonderbehandlung notwendig, da der Linker anhand der Dateiendung und ggf. am Dateiformat erkennt, was er zu tun hat:

$ gcc msg1.a msg2.a with_libs.o -o with_libs $(pkg-config --libs caca)

Fällt die Entscheidung jedoch auf statische Bibliotheken, müssen diese nach dem Compilieren (also dem Erzeugen der Objektdateien) separat gelinkt werden. Dabei müssen in beiden Phasen zusätzliche Parameter benötigt. Der Compiliervorgang sieht so aus:

$ gcc msg1.c -c -fPIC $(pkg-config --cflags --libs caca)
$ gcc msg2.c -c -fPIC $(pkg-config --cflags --libs caca)

Entscheidend dabei ist der Parameter -fPIC. Dieser weist den Compiler an, positionsunabhängigen Code zu erzeugen, also Code, der keine Annahme darüber trifft, an welcher Stelle er in den Adressraum eines Prozesses er eingeblendet wird. Auch wenn gcc für diese Objektdateien die Endung *.o verwendet, sind diese häufig auch als *.lo-Dateien anzutreffen. Das Format ist identisch, die Endung drückt nur den Sachverhalt der Reallokierbarkeit aus. Gegen diese Dateien könnte also ganz gewöhnlich statisch gelinkt werden. Ihren wahren Sinn erhalten die Dateien jedoch erst, wenn sie zu echten, dynamischen Bibliotheken gebunden werden, was durch das Linkerflag --shared geschieht. Die erzeugten Dateien werden in der Regel als *.so-Dateien abgelegt:

$ gcc msg1.o -o msg1.so --shared
$ gcc msg2.o -o msg2.so --shared

Verwendet man nun diese Dateien im ansonsten unmodifizierten Linkeraufruf der Anwendung, wird der Code der Bibliotheksdateien nicht mehr in die erzeugte Binärdatei aufgenommen. Stattdessen werden nur Symboleinträge mit Verweis auf die tatsächlich benötigten *.so-Dateien generiert und die Ausgabedatei ist deutlich kleiner als zuvor.

$ gcc msg1.so msg2.so with_libs.o -o with_libs1 $(pkg-config --libs caca)

Das kann man belegen, indem man einfach mal eine der Shared Objects umbenennt und versucht, das Programm zu starten:

$ mv msg1.so msg1.so.old
$ ./with_libs1

./with_libs1: error while loading shared libraries: msg1.so: cannot open shared object file: No such file or directory

$ mv msg1.so.old msg1.so

Dabei wird die Bibliothek an mehreren Orten im Dateisystem mit absteigender Priorität gesucht. Diese Suche beginnt immer im aktuellen Arbeitsverzeichnis und endet in einem allgemeinen Sammelverzeichnis wie /lib oder /usr/lib. Ebenfalls anzumerken ist, dass gegen *.so-Dateien im Gegensatz zu den Objektdateien, aus denen sie entstanden sind, nicht mehr statisch gelinkt werden kann. Folgendes Kommando schlägt also fehl:

$ gcc msg1.so msg2.so with_libs.o -o with_libs2 --static $(pkg-config --libs caca)

/usr/bin/ld: attempted static link of dynamic object `msg1.so'
collect2: ld returned 1 exit status

Programme und Bibliotheken systemweit installieren

Typischer Weise besteht die Installation von Programmen und Bibliotheken gleichermaßen lediglich aus dem Kopieren aller benötigten Dateien an den richtigen Ablageort. Hinzu kommen ggf. nachgelagerte Schritte wie die Anpassung bestehender Konfigurationsdateien oder die Erzeugung von Desktopverknüpfungen. Doch mit dem Kopieren ist bereits die meiste Arbeit getan, die Frage ist nur, welche Dateien wohin kopiert werden. Je nach Betriebssystem gibt es hier unterschiedliche Bräuche und Gegebenheiten, für Unixoide sollte jedoch der Filesystem Hierarchy Standard zu Rate gezogen werden. Denn im Gegensatz zu typischen Windowsinstallationen werden die Dateien, die zusammen ein Programmpaket ausmachen unter Unix in der Regel nicht einfach in ein Verzeichnis kopiert. Stattdessen gibt es FHS unterschiedliche Ablageorte für die verschiedenen Dateien vor, die auch davon abhängen, um was für eine Anwendung es sicht handelt und wie sie installiert wird. Folgende Unterscheidungen werden dabei getroffen:

  • Lokale Programme der Systemadministration vs. Programme und Daten der Distribution
  • Systemprogramme vs. Anwendungsprogramme (im Falle von nicht-lokal nachinstallierten Programmen)

Dabei handelt es sich bei den lokal nachinstallierten Programmen und Daten um Dateien, die nicht mit den Mitteln der Paketverwaltung oder anderswie durch die Unix-Distribution installiert werden. Insbesondere die via

$ make install

oder vergleichbaren Aufrufen installierten Programme fallen hierunter. Sie werden gemäß FHS unterhalb von /usr/local installiert, wobei die Untergliederung der Verzeichnisses bewust offen gelassen wurde. In der Regel orientiert sich diese aber am Wurzelverzeichnis.

Bei den durch die Distribution installierten Anwendungen wird zwischen System- und Anwendungsprogrammen unterschieden. Erstere werden benötigt, um das System hochzufahren oder zu warten, sie müssen also physikalisch auf der lokalen Festplatte vorhanden sein, damit sie auch im Recovery-Modus der Kernels zur Verfügung stehen. Letztere setzen ein bereits funktionierendes System vorraus und können auch auf entfernten Patitionen liegen, die erst spät im Bootvorgang oder gar anschließend in das Dateisystem eingehängt werden. Dementsprechend sieht der FHS zwei Verzeichnisebenen vor: / für System- und /usr für Anwendungsprogramme.

Bezüglich der Systemprogramme gilt, dass Bibliotheken immer nach /lib kopiert werden, wobei auf 64 Bit-Systemen gerne zwischen /lib32 und /lib64 unterschieden wird, je nachdem mit welcher Wortbreite die Bibliothek kompiliert wurde. Die ausführbaren Programmdateien werden hingegen entweder nach /bin kopiert, falls sie für alle Benutzer vorgesehen sind, oder nach /sbin, falls Sie nur vom Systemadministrator ausgeführt werden sollen. Für alle Verzeichnisse gilt, dass sie nicht auf entfernte Partitionen ausgelagert werden dürfen.

Anders sieht es beim Verzeichnis /usr aus, dessen Name sich aus dem Begriff Unix System Ressources ableitet. Seine Inhalte können von mehreren Rechnern gleichzeitig genutzt werden, so dass es nicht lokal vorhanden sein muss. Dementsprechend landen darin alle Anwendungsprogramme, die nicht der grundlegenden Systemadministration dienen, was aber nicht heißt, dass nicht zwischen den öffentlichen Programmen aller Benutzer und den Administrationsprogrammen unterschieden wird. Im Gegenteil: Genau wie im Wurzelverzeichnis finden sich hier die Verzeichnisse /usr/lib (bzw. zusätzlich /usr/lib32 und /usr/lib64, die auch alle auf das selbe Verzeichnis verweisen können), /usr/bin und /usr/sbin mit analogen Bedeutungen. Hinzu kommt /usr/include zur Installation von Headerdateien.

Architekturunabhängige Dateien, also alle Dokumentationen, Ikonen und sonstige, nicht ausführbare Dateien sind nur für Anwendungsprogramme vorgesehen. Denn sie werden unterhalb von /usr/share (bzw. /usr/local/share) meist in /usr/share/<programmname> oder /usr/share/<programmname>-<version> installiert, so dass sie für Systemprogramme nicht zur Verüfung stehen. Konfigrationsdateien sind davon allerdings ausgeschlossen. Sie werden in /etc oder /etc/<programmname> abgelegt, obwohl das Verzeichnis ursprünglich nur als Sammelbehälter für alles Übrige vorgesehen war, et cetera eben.

Obwohl der FHS für Unix-Systeme vorgesehen ist, wird darin über die Strukturierung des Dateisystems hinaus keine Aussage über die Zugriffsberechtigungen der Dateien getroffen. Desse ungeachtet haben sich in der Praxis 755(7) (rwxrw-rw-) für ausführbare Dateien und 644(7) für nicht-ausführbare Dateien etabliert. Für die Softwareinstallation bedeutet dies, dass neben dem Kopieren der Datein (cp) auch diese Berechtigungen gesetzt werden müssen (chmod). Da ein Installationsskript durch den Aufruf zweier Befehle pro erzeugter Datei natürlich an Komplexität und somit an Fehlerpotential zunimmt, beinhaltet der Unix-Werkzeugkasten den Befehl install, der mehrere Schritte vereint. Er ermöglicht es, eine oder mehrere Dateien in ein Zielverzeichnis zu kopieren und dabei gleichzeitig die Zugriffsberechtigungen und die Besitzer der Dateien zu setzen. Aus

$ cp $SRC_FILE $DST_DIR
$ chmod 755 $DST_FILE
$ chown user:group $DST_FILE

wird also ganz einfach

$ install --owner=user --group=group --mode=755 $SRC_FILE $DST_DIR

bzw.

$ install -o user -g group -m 755 $SRC_FILE $DST_DIR

Dabei können bereits vorhandene Dateien gesichert werden, wozu der Parameter -b bzw. --backup in Verbindung mit -S bzw. --sufix da ist. Mit letzterem kann die Dateiendung der Backups definiert werden, z.B. so:

$ install --backup --sufix=.bak $SRC_FILE $DST_DIR

oder einfach

$ install --backup $SRC_FILE $DST_DIR

Sollen die erzeugten Dateien den selben Zeitstempel wie die Originale besitzen, ist noch der Parameter -p bzw. --preserve-timestamps zu übergeben. Weitere Variationen des Befehls bestehen hinsichtlich der Bedeutung der übergebenen Dateinamen. Wird ihnen ein -T vorangestellt, handelt es sich immer um eine Quell- und eine Zieldatei. Fehlt das -T, handelt es sich stattdessen um ein Zielverzeichnis, in welches die Quelldatei hineinkopiert wird. Ein kleines -t hingegen, sorgt dafür, dass die beiden Werte in umgekehrter Reihenfolge übergeben werden müssen: Erst das Zielverzeichnis, dann die Quelldateien.

Angewandt auf das Testprogramm heißt das, dass folgende Befehle zur Installation benötigt werden. Im Falle der einfachen Version:

$ install --mode=755 single_file /usr/bin

Und im Falle der zweiten Version mit geteilten Bibliotheken:

$ install --mode=755 single_file /usr/bin
$ install --mode=755 msg1.so msg2.so /usr/lib

Und falls die Header installiert werden sollen, noch zusätzlich:

$ install --mode=644 msg1.h msg2.h /usr/include

Die Shell, dein Freund und Helfer

<<<EIN AUSFÜHRLICHES SHELL-SCRIPT>>>

Zurück zum übergeordneten Thema Build Systeme.


attachments

imageappend Append an Image
>