[X] Build-Systeme Teil 3: SCons



  • rwerben kann man diese mit dem Tutorial auf der Python-Homepage.

    Hier wäre der Link nett.

    gutes Tutorial gibt(http://www.scons.org/doc/HTML/scons-user.html)

    Mach da doch auch einen Textlink draus, für Faule und sieht besser aus.

    Für Windows gibt es einen Installer, der wohl keiner weiterer Erklärung bedarf

    Wo?

    Sonst ist es für mich zu fremd um viel sagen zu können. 😞
    Ich denke aber, ich habe es grundlegend verstanden. 👍

    Warte nicht zu lange mit der Rechtschreibfreigabe, sonst wird das zu knapp. ⚠



  • Im nunmehr 3. Teil der Serie "Build-Systeme" werde ich dieses mal SCons vorstellen.

    Inhalt:

    • 1 Hintergründe zu SCons
    • 2 Installation von SCons
    • 3 Die Datei "SConstruct"
    • 4 Erstellen von Programmen
    • 4.1 Keyword Arguments
    • 4.2 Mehrere Quelldateien
    • 5 Erstellen von Bibliotheken
    • 6 Einbinden von Bibliotheken
    • 7 Abhängigkeiten erkennen
    • 8 Zum Schluss
    • 9 What comes next...

    **
    1 Hintergründe zu SCons
    **

    SCons ist ein Open-Source Build-System, welches in Python implementiert wurde und seine Wurzeln im Perl-basierten Cons hat. Bei der Entwicklung von SCons wurde auf Korrektheit, Geschwindigkeit und besondere Einfachheit Wert gelegt. Wie Python selbst, ist SCons sehr einfach zu benutzen, sowohl für einfache Aufgaben, als auch für komplizierte Builds.
    Um SCons zu benutzen braucht es nur minimale Python-Kenntnisse. Erwerben kann man diese mit dem Tutorial auf der Python-Homepage.

    SCons kann sowohl für C bzw. C++ als auch für Java verwendet werden, in diesem Artikel werde ich allerdings nur auf C++ eingehen und die Java-Anhänger auf das offizielle Tutorial verweisen.

    Da es von den Herstellern von SCons bereits ein sehr gutes Tutorial gibt, werde ich bloß auf die grundlegenden Funktionen eingehen und diese relativ schnell abhandeln.

    **
    2 Installation von SCons
    **

    SCons ist, da auf Python-basierend, plattformunabhängig. Um mit SCons zu arbeiten, muss Python installiert sein.
    Man kann entweder die vorkompilierten Pakete installieren, oder es selbst kompilieren. Die unten erwähnten Pakete kann man in der "Download"-Section von http://www.scons.org/ finden.

    Für Windows gibt es, wie gewohnt, einen Installer, der wohl keiner weiterer Erklärung bedarf 😉

    Auf RPM-basierten Systemen geht die Installation relativ simpel:

    rpm -uvh scons-x.x-x.noarch.rpm
    

    Auch bei Debian-basierten Systemen ist es nicht sehr schwer:

    apt-get install scons
    

    Wer SCons aus den Quellen übersetzen möchte, lädt sich den Tarball (oder das zip-File) herunter, entpackt ihn und wechselt ins scons Verzeichnis. Dort einfach

    python setup.py install
    

    eingeben und SCons wird in die üblichen Verzeichnisse ( /usr/local/ )installiert. Dies erfordert allerdings Administrator-Rechte, besitzt man diese nicht, so kann man folgendermaßen ein anderes Verzeichnis angeben:

    python setup.py install --prefis=$HOME
    

    **
    3 Die Datei "SConstruct"
    **

    Diese Datei kann man in etwa mit einem Makefile oder einer Ant - XML Datei vergleichen, denn in ihr stehen die Tasks und Targets, die SCons dann ausführt.
    Um die Aufgaben zu beschreiben, wird Python verwendet, d.h. dass diese Datei eigentlich ein Python-Skript ist. Im Unterschied zu Python-Skripten wird diese Datei aber nicht unbedingt geordnet abgearbeitet (siehe unten), sondern so, wie es die Erfüllung der Targets erfordert.
    Die "SConstruct" sollte man an der Wurzel im Projektverzeichnis abgelegen.

    **
    4 Erstellen von Programmen
    **

    Um zu zeigen, wie man mit SCons Programme erstellen kann, werden wir das allseits beliebte "Hallo Welt" Beispiel verwenden:

    //hello_world.cpp
    #include <iostream>
    
    int main(int argc, char **argv) {
      std::cout<<"Hello World\n";
      return EXIT_SUCCESS;
    };
    

    Sehen wir uns die dazugehörige SConstruct-Datei an:

    #SConstruct
    #Kommentare beginnen in Python mit einem #-Zeichen
    Program("hello_world.cpp")
    

    Diese Anweisung sagt SCons zwei Dinge: Wir wollen (1) ein Programm erstellen, und zwar (2) aus der Datei "hello_world.cpp". Um den Build-Prozess zu starten, wechselt man in das Verzeichnis, in dem die Datei "SConstruct" liegt und tippt einfach

    scons
    

    in die Shell ein.
    Auf meinem Linux-System erscheinet folgende Ausgabe:

    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    

    Nun wurden eine hello_world.o und eine Binary namens hello_world erstellt. Auf Windows-Systemen sollten die Dateien hello_world.obj und hello_world.exe heißen.
    Es ist im Übrigen möglich, in einer SConstruct mehrere Targets zu definieren, also z.B. zwei oder drei Programme zu erstellen (wobei die Reihenfolge, wie SCons die Builds durchführt, nicht der Datei SConstruct folgen muss, d.h. es könnte sein, dass KillerApplication vor hello_world erstellt wird):

    Program("hello_world.cpp") #Erstes Target
    Program("KillerApplication.cpp") #Zweites Target
    

    Möchte man nur Objekt-Dateien erstellen, muss man das Target in der SConstruct leicht verändern:

    #SConstruct
    #Einfache Anführungszeichen gehen auch:
    Object('hello_world.cpp')
    

    Um den Effekt zu sehen, müssen wir zuerst die Ergebnisse des vorangegangene Builds löschen. Dies geschieht mit einem Aufruf von scons und dem -c (oder --clean) Parameter:

    scons -c
    

    Führt man scons nun erneut aus, so wird nur eine hello_world.o bzw. hello_world.obj erstellt.

    Die Ausgaben, die SCons um die eigentlichen Befehle erzeugt, können leicht mit dem -Q Parameter unterdrückt werden, um nur die wirklich wichtigen Informationen zu sehen:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o hello_world.o hello_world.cpp
    

    Bisher hat SCons immer die Namen für die erzeugten Programme vergeben, das können wir aber auch selbst:

    #SConstruct
    Program("Hello", "hello_world.cpp")
    

    Aus hello_world.cpp wird nun eine Binary namens Hello erstellt:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -o Hello hello_world.o
    gpc@darkstar:~/tmp$ ls -l
    total 28
    -rwxr-xr-x  1 gpc users 12755 2005-12-18 16:38 Hello*
    -rw-r--r--  1 gpc users    36 2005-12-18 16:37 SConstruct
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:31 hello_world.o
    

    Man sieht, es wurde eine Hello erstellt.

    4.1 Keyword Arguments
    *

    Wenn man die Quelldateien zuerst auflisten möchte, und dann erst den Namen des Targets, so muss man mit so genannten "Keyword Arguments" arbeiten:

    #Standardreihenfolge
    Program(target = Hello, source = "hello_world.cpp")
    
    #Umgekehrte Reihenfolge
    Program(source = "hello_world.cpp", target = Hello)
    

    4.2 Mehrere Quelldateien
    *

    Größere Programme bestehen selten aus einer einzelnen Quelldatei, sollten sie jedenfalls nicht. SCons gibt uns selbstverständlich die Möglichkeit, mehrere Quelldateien zu kompilieren:

    #SConstruct
    #Nehmen wir an, es gibt noch eine goodbye.cpp
    Program("Hello", ["hello_world.cpp", "goodbye.cpp"])
    

    Ein Aufruf zeigt uns folgendes:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o goodbye.o goodbye.cpp
    g++ -o Hello hello_world.o goodbye.o
    gpc@darkstar:~/tmp$ ls -l
    total 32
    -rwxr-xr-x  1 gpc users 12799 2005-12-18 16:51 Hello*
    -rw-r--r--  1 gpc users    53 2005-12-18 16:51 SConstruct
    -rw-r--r--  1 gpc users     0 2005-12-18 16:51 goodbye.cpp
    -rw-r--r--  1 gpc users   625 2005-12-18 16:51 goodbye.o
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:40 hello_world.o
    

    Hier sieht man, dass die Datei goodbye.cpp erfolgreich kompliert und in Hello gelinkt wurde.

    Dieses Konstrukt mit den eckigen Klammern in der SConstruct, ist eine Python-Liste, welche die einzelnen Quelldateien beinhaltet. Es wäre auch möglich, nur einen Eintrag in die Liste zu setzen, dies hätte den gleichen Effekt wie vorher, als wir nur einen String mit dem Namen übergeben haben:

    #Beide Statements haben den gleichen Effekt
    Program("Main", ["main.cpp"])
    #Program("Main", "main.cpp")
    

    Da das Schreiben und Lesen einer längeren Liste nicht sehr viel Spaß macht, kann man alternativ auch die Python Split-Funktion aus dem string-Modul verwenden:

    #SConstruct
    Program("Calculator", Split("main.cpp calculator.cpp parser.cpp"))
    

    Auch möglich:

    #SConstruct
    fileList = Split("main.cpp calculator.cpp parser.cpp")
    Program("Calculator", fileList)
    

    Über mehrere Zeilen:

    #SConstruct
    fileList = Split("""main.cpp 
    		    calculator.cpp 
    		    parser.cpp""")
    Program("Calculator", fileList)
    

    Im letzten Beispiel wurde die "triple-quote" Syntax verwendet, die es erlaubt, einen String über mehrere Zeilen hinweg zu definieren. Auch hier können es entweder drei doppelte oder drei einzelne Anführungszeichen sein.

    **
    5 Erstellen von Bibliotheken
    **

    Abgesehen von Programmen und Objektdateien kann SCons auch mit Leichtigkeit Bibliotheken erstellen, sowohl statische als auch dynamische.

    Erstellen wir eine kleine Bibliothek, die in je einer Quelldatei eine Grundrechenart bereitstellt (die Deklaration findet sich in den dazugehörigen *.hpp Dateien):

    #SConstruct
    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Library erstellt eine statische Bibltiothek
    Library(target="math", source=fileList)
    

    Um nun die "Bibliothek" zu erstellen, rufen wir scons auf, was folgende Ausgabe produziert:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o add.o add.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o mul.o div.o
    ranlib libmath.a
    ar: creating libmath.a
    

    Unsere Bibliothek wurde erfolgreich erstellt.

    Wer sich gerne exakt ausdrückt, der kann anstatt Library auch StaticLibrary schreiben, wenn er eine statische Bibliothek erstellen möchte. Ein Unterschied existiert nicht.

    Das Erstellen einer dynamischen Bibliothek (*.so auf POSIX-Systemen, *.dll auf Windows-Systemen) ist genauso einfach:

    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Nun erstellen wir eine dynamische Bibliothek
    SharedLibrary(target="math", source=fileList)
    

    Führen wir nun SCons aus:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -fPIC -c -o add.os add.cpp
    g++ -fPIC -c -o div.os div.cpp
    g++ -fPIC -c -o sub.os sub.cpp
    g++ -fPIC -c -o mul.os mul.cpp
    g++ -shared -o libmath.so add.os sub.os mul.os div.os
    

    Wie wir sehen, hat SCons eine dynamische Bibliothek erstellt, und das mit so wenig Aufwand.

    **
    6 Einbinden von Bibliotheken
    **

    Natürlich wäre die beste Bibliothek nutzlos, wenn man sie nicht in seinen Programmen verwenden würde. Damit dies möglich ist, muss man die Bibliothek natürlich in die Executable hineinlinken.

    Die main.cpp sieht so aus:

    #include <iostream>
    #include "add.hpp"
    
    int main(int argc, char **argv) {
      int result = add(2,7);
      std::cout<<result<<'\n';
      return EXIT_SUCCESS;
    };
    

    Die SConstruct so:

    #SConstruct
    Library("math", ["add.cpp", "sub.cpp", "div.cpp", "mul.cpp"])
    Program("main.cpp", LIBS=["math"], LIBPATH=".")
    

    Bemerkenswert ist, dass wir den Namen der Bibliothek einfach so angeben können, ohne uns über systemspezifische Präfixe Gedanken machen zu müssen.

    Wenn wir jetzt scons aufrufen, erhalten wir folgende Ausgabe:

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o add.o add.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o div.o mul.o
    ranlib libmath.a
    ar: creating libmath.a
    g++ -c -o main.o main.cpp
    g++ -o main main.o -L. -lmath
    scons: done building targets.
    

    Über das oben verwendete LIBPATH Argument sucht SCons selbstständig nach Bibliotheken in den angegebenen Verzeichnissen:

    #SConstruct
    Program("Test", LIBS = "someLib", LIBPATH = ['/usr/lib', '/usr/local/lib'])
    

    SCons sucht nun in den beiden Verzeichnissen, ob es eine "someLib" Bibliothek findet. Kann keine Bibliothek gefunden werden, meldet sich SCons mit einer Fehlermeldung.

    **
    7 Abhängigkeiten erkennen
    **

    Kommen wir zu einem interessanteren Feature: der Erkennung von Abhängigkeiten. Wenn sich an unserem Programm etwas geändert hat und wir es neu kompilieren, dann wollen wir, dass nur das nötigste neu übersetzt wird und Teile, die sich nicht geändert haben, unangetastet bleiben.

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    
    gpc@darkstar:~/tmp$ scons -Q
    scons: `.' is up to date.
    

    SCons hat erkannt, dass seit dem letzten Build keine Änderungen stattgefunden haben und hat gar nichts gemacht. Soweit so gut.
    Standardmäßig erkennt SCons eine Änderung am Inhalt der Datei, nicht am Datum des letzten Zugriffs (wie make es macht).

    Will man das Verhalten von make, muss man die SourceSignatures Funktion benutzen:

    #SConstruct
    Program("hello_world.cpp")
    
    #Standardverhalten:
    #SourceSignatures("MD5")
    
    #make-Verhalten:
    SourceSignatures("timestamp")
    

    Sofern ein Target von einem anderen abhängt (z.B. Programm von Bibliothek), kann man auch hier festlegen, anhand welcher Kriterien SCons über einen Rebuild entscheidet. Dies geschieht mittels der TargetSignatures Funktion.
    Das Standardverhalten ist, dass SCons anhand einer Target-Signatur, die aus den Signaturen der Quelldateien besteht, entscheidet ob ein Rebuild notwendig ist:

    Program("hello_world.cpp")
    TargetSignatures("build")
    

    Wenn sich eine Quelldatei aber so verändert hat, dass der Inhalt des Target der gleiche bleibt, dann ist kein kompletter Rebuild notwendig. Dieses Verhalten kann man so erreichen:

    Program("hello_world.cpp")
    TargetSignatures("content")
    

    Nun ist es gängige Praxis, die Schnittstelle einer Klasse in eine Header-Datei zu schreiben und die Implementation in eine Source-Datei auszulagern. Sehen wir uns folgendes kleines Programm an:

    //foo.hpp
    #ifndef FOO_HPP
    #define FOO_HPP
    
    struct Foo {
      void bar() const;
    };
    
    #endif
    
    //foo.cpp
    #include "foo.hpp"
    #include <iostream>
    
    void Foo::bar() const {
      std::cout<<"Blub blub\n";
    };
    
    //hello_world.cpp
    #include "foo.hpp"
    
    int main() {
      Foo f;
      f.bar();
      return 0;
    };
    

    Wenn sich an foo.hpp etwas ändert, will ich, dass SCons dass erkennt und einen Rebuild durchführt, und das sage ich SCons mit der CPPPATH Variable:

    #SConstruct
    Program(['hello_world.cpp','foo.cpp'], CPPPATH = '.')
    

    Nun sieht SCons im aktuellen (".") Verzeichnis nach, um herauszufinden ob ein Rebuild notwendig ist.
    Analog zu der LIBPATH Variable, kann die CPPPATH Variable eine Liste sein:

    #SConstruct
    Program('hello_world.cpp', CPPPATH = ['.', '/home/gpc/tmp/include'])
    

    **
    8 Zum Schluss
    **

    Das war jetzt nur eine kleine Vorstellung von dem, was SCons kann. Alles darzustellen war aber auch nicht mein Anspruch, deshalb sei an dieser Stelle nochmal auf das offizielle SCons-Tutorial verwiesen, welches sehr viele Informationen enthält und auf (fast) alle Fragen eine Antwort bietet.

    **
    9 What comes next...
    **

    Im nächsten Artikel wird uns Artchi dann bjam, das Build-System von boost, erklären.



  • estartu schrieb:

    rwerben kann man diese mit dem Tutorial auf der Python-Homepage.

    Hier wäre der Link nett.

    Erledigt

    gutes Tutorial gibt(http://www.scons.org/doc/HTML/scons-user.html)

    Mach da doch auch einen Textlink draus, für Faule und sieht besser aus.

    Erledigt

    Für Windows gibt es einen Installer, der wohl keiner weiterer Erklärung bedarf

    Wo?

    Ich vermeide einen festen Link, da der in Abhängigkeit der Versionsnummer existiert, bsp: http://prdownloads.sourceforge.net/scons/scons-0.96.1.win32.exe
    Hab aber in der Zeile drüber nen Verweis eingebaut.

    Warte nicht zu lange mit der Rechtschreibfreigabe, sonst wird das zu knapp. ⚠

    Ich weiß (mach ich heute noch), ich befürchte sowieso, dass wir diesemal nicht 3 Artikel zusammenkriegen.

    MfG

    GPC



  • Ich hab's jetzt mal auf R gesetzt, also los ihr Rechtschreibfreaks 😉
    EDIT: Revidiert.

    Solltet ihr oder irgendjemand anders noch Anmerkungen technischer Natur haben, nur her damit, ich werd's dann schon irgendwie zusammenkleben.
    Irgendwie bin ich noch unzufrieden mit dem Artikel...



  • Wenn du ihn noch nicht veröffentlichen willst, dann warte noch.
    Es hilft uns nix, zwar schon jeden Monat was zu veröffentlichen, was sich hinterher als unreif entpuppt.

    Kannst du das

    Irgendwie bin ich noch unzufrieden mit dem Artikel...

    irgendwie so in Worte fassen, dass man dir helfen kann? 🙂



  • GPC schrieb:

    Ich hab's jetzt mal auf R gesetzt, also los ihr Rechtschreibfreaks 😉

    Solltet ihr oder irgendjemand anders noch Anmerkungen technischer Natur haben, nur her damit, ich werd's dann schon irgendwie zusammenkleben.
    Irgendwie bin ich noch unzufrieden mit dem Artikel...

    Ich fänd es super wenn du auch was zum Thema testen auf Libs schreiben könntest
    ( z.b. libs die nicht pkg-config nutzen )
    oder z.b. wie man herausfindet ob sich in den angegebenen CFlags auch ein bestimmter Header erreichbar ist.
    Und damit meine ich jetzt die nicht Standard Header.
    beispielsweise testen ob boost/cstdint.hpp erreichbar ist, oder boost/format.hpp etc. sachen die keine Libs haben.
    ( Macht man meist durch den Versuch Code zu kompilieren )

    Nur mal so als Anregung.

    BR
    evilissimo



  • Kommando zurück, doch noch keine R-Prüfung. Sorry.

    estartu schrieb:

    Wenn du ihn noch nicht veröffentlichen willst, dann warte noch.
    Es hilft uns nix, zwar schon jeden Monat was zu veröffentlichen, was sich hinterher als unreif entpuppt.

    Morgen mittag habe ich Zeit, da kann ich ihn fertig machen, damit's dann wirklich zur R-Prüfung kann.

    Kannst du das

    Irgendwie bin ich noch unzufrieden mit dem Artikel...

    irgendwie so in Worte fassen, dass man dir helfen kann? 🙂

    Er fühlt sich irgendwie noch nicht rund an...aber wie gesagt, bis morgen hab ich ihn rund.

    evilissimo schrieb:

    Ich fänd es super wenn du auch was zum Thema testen auf Libs schreiben könntest
    ( z.b. libs die nicht pkg-config nutzen )

    Blub? Komm nicht ganz mit 😕 Eigentlich macht es in dem Sinn keinen Unterschied ob du pkg-config benutzt oder nicht.

    oder z.b. wie man herausfindet ob sich in den angegebenen CFlags auch ein bestimmter Header erreichbar ist.
    Und damit meine ich jetzt die nicht Standard Header.
    beispielsweise testen ob boost/cstdint.hpp erreichbar ist, oder boost/format.hpp etc. sachen die keine Libs haben.
    ( Macht man meist durch den Versuch Code zu kompilieren )

    Also praktisch eine Abfrage, ob Header Foo aus Bibliothek bar bei dem User installiert ist?
    Ich schau mal was ich machen kann, morgen. Denn nach 6h BWL und 3h VWL hat man einfach die Schnauze voll. 👎

    MfG

    GPC



  • Im nunmehr 3. Teil der Serie "Build-Systeme" werde ich dieses mal SCons vorstellen.

    Inhalt:

    • 1 Hintergründe zu SCons
    • 2 Installation von SCons
    • 3 Die Datei "SConstruct"
    • 4 Erstellen von Programmen
    • 4.1 Keyword Arguments
    • 4.2 Mehrere Quelldateien
    • 5 Erstellen von Bibliotheken
    • 6 Einbinden von Bibliotheken
    • 7 Abhängigkeiten erkennen
    • 8 Auf Abhängigkeiten testen
    • 8.1 Construction Environments
    • 8.2 Auf Bibliotheken/Header/Funktionen/typedefs testen
    • 9 Zum Schluss
    • 10 What comes next...

    **
    1 Hintergründe zu SCons
    **

    SCons ist ein Open-Source Build-System, welches in Python implementiert wurde und seine Wurzeln im Perl-basierten Cons hat. Bei der Entwicklung von SCons wurde auf Korrektheit, Geschwindigkeit und besondere Einfachheit Wert gelegt. Wie Python selbst, ist SCons sehr einfach zu benutzen, sowohl für einfache Aufgaben, als auch für komplizierte Builds.
    Um SCons zu benutzen braucht es nur minimale Python-Kenntnisse. Erwerben kann man diese mit dem Tutorial auf der Python-Homepage.

    SCons kann für so ziemlich jede Sprache verwendet werden, in diesem Artikel werde ich allerdings nur auf C bzw. C++ eingehen und z.B. die Java-Anhänger auf das offizielle Tutorial verweisen.

    Da es von den Herstellern von SCons bereits ein sehr gutes Tutorial gibt, werde ich bloß auf die grundlegenden Funktionen eingehen und diese relativ schnell abhandeln.

    **
    2 Installation von SCons
    **

    SCons ist, da auf Python-basierend, plattformunabhängig. Um mit SCons zu arbeiten, muss Python installiert sein.
    Man kann entweder die vorkompilierten Pakete installieren, oder es selbst kompilieren. Die unten erwähnten Pakete kann man in der "Download"-Section von http://www.scons.org/ finden.

    Für Windows gibt es, wie gewohnt, einen Installer, der wohl keiner weiterer Erklärung bedarf 😉

    Auf RPM-basierten Systemen geht die Installation relativ simpel:

    rpm -uvh scons-x.x-x.noarch.rpm
    

    Auch bei Debian-basierten Systemen ist es nicht sehr schwer:

    apt-get install scons
    

    Wer SCons aus den Quellen übersetzen möchte, lädt sich den Tarball (oder das zip-File) herunter, entpackt ihn und wechselt ins scons Verzeichnis. Dort einfach

    python setup.py install
    

    eingeben und SCons wird in die üblichen Verzeichnisse ( /usr/local/ )installiert. Dies erfordert allerdings Administrator-Rechte, besitzt man diese nicht, so kann man folgendermaßen ein anderes Verzeichnis angeben:

    python setup.py install --prefix=$HOME
    

    **
    3 Die Datei "SConstruct"
    **

    Diese Datei kann man in etwa mit einem Makefile oder einer Ant - XML Datei vergleichen, denn in ihr stehen die Tasks und Targets, die SCons dann ausführt.
    Um die Aufgaben zu beschreiben, wird Python verwendet, d.h. dass diese Datei eigentlich ein Python-Skript ist. Im Unterschied zu Python-Skripten wird diese Datei aber nicht unbedingt geordnet abgearbeitet (siehe unten), sondern so, wie es die Erfüllung der Targets erfordert.
    Die "SConstruct" sollte man an der Wurzel im Projektverzeichnis abgelegen.

    **
    4 Erstellen von Programmen
    **

    Um zu zeigen, wie man mit SCons Programme erstellen kann, werden wir das allseits beliebte "Hallo Welt" Beispiel verwenden:

    //hello_world.cpp
    #include <iostream>
    
    int main(int argc, char **argv) {
      std::cout<<"Hello World\n";
      return EXIT_SUCCESS;
    };
    

    Sehen wir uns die dazugehörige SConstruct-Datei an:

    #SConstruct
    #Kommentare beginnen in Python mit einem #-Zeichen
    Program("hello_world.cpp")
    

    Diese Anweisung sagt SCons zwei Dinge: Wir wollen (1) ein Programm erstellen, und zwar (2) aus der Datei "hello_world.cpp". Um den Build-Prozess zu starten, wechselt man in das Verzeichnis, in dem die Datei "SConstruct" liegt und tippt einfach

    scons
    

    in die Shell ein.
    Auf meinem Linux-System erscheinet folgende Ausgabe:

    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    

    Nun wurden eine hello_world.o und eine Binary namens hello_world erstellt. Auf Windows-Systemen sollten die Dateien hello_world.obj und hello_world.exe heißen.
    Es ist im Übrigen möglich, in einer SConstruct mehrere Targets zu definieren, also z.B. zwei oder drei Programme zu erstellen (wobei die Reihenfolge, wie SCons die Builds durchführt, nicht der Datei SConstruct folgen muss, d.h. es könnte sein, dass KillerApplication vor hello_world erstellt wird):

    Program("hello_world.cpp") #Erstes Target
    Program("KillerApplication.cpp") #Zweites Target
    

    Möchte man nur Objekt-Dateien erstellen, muss man das Target in der SConstruct leicht verändern:

    #SConstruct
    #Einfache Anführungszeichen gehen auch:
    Object('hello_world.cpp')
    

    Um den Effekt zu sehen, müssen wir zuerst die Ergebnisse des vorangegangene Builds löschen. Dies geschieht mit einem Aufruf von scons und dem -c (oder --clean) Parameter:

    scons -c
    

    Führt man scons nun erneut aus, so wird nur eine hello_world.o bzw. hello_world.obj erstellt.

    Die Ausgaben, die SCons um die eigentlichen Befehle erzeugt, können leicht mit dem -Q Parameter unterdrückt werden, um nur die wirklich wichtigen Informationen zu sehen:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o hello_world.o hello_world.cpp
    

    Bisher hat SCons immer die Namen für die erzeugten Programme vergeben, das können wir aber auch selbst:

    #SConstruct
    Program("Hello", "hello_world.cpp")
    

    Aus hello_world.cpp wird nun eine Binary namens Hello erstellt:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -o Hello hello_world.o
    gpc@darkstar:~/tmp$ ls -l
    total 28
    -rwxr-xr-x  1 gpc users 12755 2005-12-18 16:38 Hello*
    -rw-r--r--  1 gpc users    36 2005-12-18 16:37 SConstruct
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:31 hello_world.o
    

    Man sieht, es wurde eine Hello erstellt.

    4.1 Keyword Arguments
    *

    Wenn man die Quelldateien zuerst auflisten möchte, und dann erst den Namen des Targets, so muss man mit so genannten "Keyword Arguments" arbeiten:

    #Standardreihenfolge
    Program(target = Hello, source = "hello_world.cpp")
    
    #Umgekehrte Reihenfolge
    Program(source = "hello_world.cpp", target = Hello)
    

    4.2 Mehrere Quelldateien
    *

    Größere Programme bestehen selten aus einer einzelnen Quelldatei, sollten sie jedenfalls nicht. SCons gibt uns selbstverständlich die Möglichkeit, mehrere Quelldateien zu kompilieren:

    #SConstruct
    #Nehmen wir an, es gibt noch eine goodbye.cpp
    Program("Hello", ["hello_world.cpp", "goodbye.cpp"])
    

    Ein Aufruf zeigt uns folgendes:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o goodbye.o goodbye.cpp
    g++ -o Hello hello_world.o goodbye.o
    gpc@darkstar:~/tmp$ ls -l
    total 32
    -rwxr-xr-x  1 gpc users 12799 2005-12-18 16:51 Hello*
    -rw-r--r--  1 gpc users    53 2005-12-18 16:51 SConstruct
    -rw-r--r--  1 gpc users     0 2005-12-18 16:51 goodbye.cpp
    -rw-r--r--  1 gpc users   625 2005-12-18 16:51 goodbye.o
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:40 hello_world.o
    

    Hier sieht man, dass die Datei goodbye.cpp erfolgreich kompliert und in Hello gelinkt wurde.

    Dieses Konstrukt mit den eckigen Klammern in der SConstruct, ist eine Python-Liste, welche die einzelnen Quelldateien beinhaltet. Es wäre auch möglich, nur einen Eintrag in die Liste zu setzen, dies hätte den gleichen Effekt wie vorher, als wir nur einen String mit dem Namen übergeben haben:

    #Beide Statements haben den gleichen Effekt
    Program("Main", ["main.cpp"])
    #Program("Main", "main.cpp")
    

    Da das Schreiben und Lesen einer längeren Liste nicht sehr viel Spaß macht, kann man alternativ auch die Python Split-Funktion aus dem string-Modul verwenden:

    #SConstruct
    Program("Calculator", Split("main.cpp calculator.cpp parser.cpp"))
    

    Auch möglich:

    #SConstruct
    fileList = Split("main.cpp calculator.cpp parser.cpp")
    Program("Calculator", fileList)
    

    Über mehrere Zeilen:

    #SConstruct
    fileList = Split("""main.cpp 
    		    calculator.cpp 
    		    parser.cpp""")
    Program("Calculator", fileList)
    

    Im letzten Beispiel wurde die "triple-quote" Syntax verwendet, die es erlaubt, einen String über mehrere Zeilen hinweg zu definieren. Auch hier können es entweder drei doppelte oder drei einzelne Anführungszeichen sein.

    **
    5 Erstellen von Bibliotheken
    **

    Abgesehen von Programmen und Objektdateien kann SCons auch mit Leichtigkeit Bibliotheken erstellen, sowohl statische als auch dynamische.

    Erstellen wir eine kleine Bibliothek, die in je einer Quelldatei eine Grundrechenart bereitstellt (die Deklaration findet sich in den dazugehörigen *.hpp Dateien):

    #SConstruct
    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Library erstellt eine statische Bibltiothek
    Library(target="math", source=fileList)
    

    Um nun die "Bibliothek" zu erstellen, rufen wir scons auf, was folgende Ausgabe produziert:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o add.o add.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o mul.o div.o
    ranlib libmath.a
    ar: creating libmath.a
    

    Unsere Bibliothek wurde erfolgreich erstellt.

    Wer sich gerne exakt ausdrückt, der kann anstatt Library auch StaticLibrary schreiben, wenn er eine statische Bibliothek erstellen möchte. Ein Unterschied existiert nicht.

    Das Erstellen einer dynamischen Bibliothek (*.so auf POSIX-Systemen, *.dll auf Windows-Systemen) ist genauso einfach:

    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Nun erstellen wir eine dynamische Bibliothek
    SharedLibrary(target="math", source=fileList)
    

    Führen wir nun SCons aus:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -fPIC -c -o add.os add.cpp
    g++ -fPIC -c -o div.os div.cpp
    g++ -fPIC -c -o sub.os sub.cpp
    g++ -fPIC -c -o mul.os mul.cpp
    g++ -shared -o libmath.so add.os sub.os mul.os div.os
    

    Wie wir sehen, hat SCons eine dynamische Bibliothek erstellt, und das mit so wenig Aufwand.

    **
    6 Einbinden von Bibliotheken
    **

    Natürlich wäre die beste Bibliothek nutzlos, wenn man sie nicht in seinen Programmen verwenden würde. Damit dies möglich ist, muss man die Bibliothek natürlich in die Executable hineinlinken.

    Die main.cpp sieht so aus:

    #include <iostream>
    #include "add.hpp"
    
    int main(int argc, char **argv) {
      int result = add(2,7);
      std::cout<<result<<'\n';
      return EXIT_SUCCESS;
    };
    

    Die SConstruct so:

    #SConstruct
    Library("math", ["add.cpp", "sub.cpp", "div.cpp", "mul.cpp"])
    Program("main.cpp", LIBS=["math"], LIBPATH=".")
    

    Bemerkenswert ist, dass wir den Namen der Bibliothek einfach so angeben können, ohne uns über systemspezifische Präfixe Gedanken machen zu müssen.

    Wenn wir scons jetzt aufrufen, erhalten wir folgende Ausgabe:

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o add.o add.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o div.o mul.o
    ranlib libmath.a
    ar: creating libmath.a
    g++ -c -o main.o main.cpp
    g++ -o main main.o -L. -lmath
    scons: done building targets.
    

    Über das oben verwendete LIBPATH Argument sucht SCons selbstständig nach Bibliotheken in den angegebenen Verzeichnissen:

    #SConstruct
    Program("Test", LIBS = "someLib", LIBPATH = ['/usr/lib', '/usr/local/lib'])
    

    SCons sucht nun in den beiden Verzeichnissen, ob es eine "someLib" Bibliothek findet. Kann keine Bibliothek gefunden werden, meldet sich SCons mit einer Fehlermeldung.

    **
    7 Abhängigkeiten erkennen
    **

    Kommen wir zu einem interessanteren Feature: der Erkennung von Abhängigkeiten. Wenn sich an unserem Programm etwas geändert hat und wir es neu kompilieren, dann wollen wir, dass nur das nötigste neu übersetzt wird und Teile, die sich nicht geändert haben, unangetastet bleiben.

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    
    gpc@darkstar:~/tmp$ scons -Q
    scons: `.' is up to date.
    

    SCons hat erkannt, dass seit dem letzten Build keine Änderungen stattgefunden haben und hat gar nichts gemacht. Soweit so gut.
    Standardmäßig erkennt SCons eine Änderung am Inhalt der Datei, nicht am Datum des letzten Zugriffs (wie make es macht).

    Will man das Verhalten von make, muss man die SourceSignatures Funktion benutzen:

    #SConstruct
    Program("hello_world.cpp")
    
    #Standardverhalten:
    #SourceSignatures("MD5")
    
    #make-Verhalten:
    SourceSignatures("timestamp")
    

    Sofern ein Target von einem anderen abhängt (z.B. Programm von Bibliothek), kann man auch hier festlegen, anhand welcher Kriterien SCons über einen Rebuild entscheidet. Dies geschieht mittels der TargetSignatures Funktion.
    Das Standardverhalten ist, dass SCons anhand einer Target-Signatur, die aus den Signaturen der Quelldateien besteht, entscheidet ob ein Rebuild notwendig ist:

    Program("hello_world.cpp")
    TargetSignatures("build")
    

    Wenn sich eine Quelldatei aber so verändert hat, dass der Inhalt des Target der gleiche bleibt, dann ist kein kompletter Rebuild notwendig. Dieses Verhalten kann man so erreichen:

    Program("hello_world.cpp")
    TargetSignatures("content")
    

    Nun ist es gängige Praxis, die Schnittstelle einer Klasse in eine Header-Datei zu schreiben und die Implementation in eine Source-Datei auszulagern. Sehen wir uns folgendes kleines Programm an:

    //foo.hpp
    #ifndef FOO_HPP
    #define FOO_HPP
    
    struct Foo {
      void bar() const;
    };
    
    #endif
    
    //foo.cpp
    #include "foo.hpp"
    #include <iostream>
    
    void Foo::bar() const {
      std::cout<<"Blub blub\n";
    };
    
    //hello_world.cpp
    #include "foo.hpp"
    
    int main() {
      Foo f;
      f.bar();
      return 0;
    };
    

    Wenn sich an foo.hpp etwas ändert, will ich, dass SCons dass erkennt und einen Rebuild durchführt, und das sage ich SCons mit der CPPPATH Variable:

    #SConstruct
    Program(['hello_world.cpp','foo.cpp'], CPPPATH = '.')
    

    Nun sieht SCons im aktuellen (".") Verzeichnis nach, um herauszufinden ob ein Rebuild notwendig ist.
    Analog zu der LIBPATH Variable, kann die CPPPATH Variable eine Liste sein:

    #SConstruct
    Program('hello_world.cpp', CPPPATH = ['.', '/home/gpc/tmp/include'])
    

    **
    8 Auf Abhängigkeiten testen
    **

    Viele Programme benutzen externe Bibliotheken, um fehlende Funktionalität einer Standardbibliothek auszugleichen. Wenn der Benutzer unser Programm benutzen will, dann braucht er ebenfalls diese Bibliothek, um das Programm erfolgreich kompilieren zu können. Hat er die Bibliothek nicht installiert, wird der Kompiliervorgang abbrechen, da der Compiler die Abhängigkeiten nicht auflösen kann. Nicht sehr schön. Eine elegantere Lösung wäre doch, vorher abzufragen, ob Bibliothek Foo existiert und bei nichtvorhandensein eine entsprechende Fehlermeldung auszugeben(bei den autotools wird dies von autoconf erledigt). Um dies zu bewerkstelligen, müssen wir zuerst einen kleinen Abstecher beim Thema Construction Environments machen:

    8.1 Construction Environments
    *

    Eine so genannte Construction Environment gibt uns die Möglichkeit zu bestimmen, wie unsere Programme erstellt werden, welcher Compiler genutzt wird oder welche Flags gesetzt werden.

    Mit folgendem Python-Code kann man eine Construction Environment erstellen:

    #SConstruct
    env = Environment()
    

    Hierbei wird die Construction Environment 'env' mit den auf dem System installierten Tools initialisiert, also z.B. wird bei installiertem g++ der CC-Wert auf g++ gesetzt, ebenso sieht es mit dem Linker und den anderen, für den Build-Prozess benötigten Daten aus.
    Aber es ist halt immer das gleiche mit Default-Werten: Meistens wollen wir spezielle Parameter übergeben. Nun, das ist ein leichtes:

    #SConstruct
    env = Environment(CC = 'gcc', CCFLAGS = '-O2 -g')
    
    #Ersetzen von gcc durch g++, ist ja ein cpp file
    env.Replace(CC = 'g++')
    
    #Anhaengen von -W Option
    #Wuerde CCFLAGS noch nicht existieren, wuerde es angelegt werden
    env.Append(CCFLAGS = ' -W')
    
    #Alternativ kann man Prepend zum Voranstellen benutzen:
    env.Prepend(CCFLAGS = ' -Weffc++ ')
    
    #Achtung: Neuer Aufruf:
    env.Program('main.cpp')
    

    An der Ausgabe erkennen wir, dass scons unsere Angaben übernommen hat:

    bash-3.00$ scons -Q
    g++ -Weffc++ -O2 -g -W -c -o main.o main.cpp
    g++ -o main main.o
    

    Eine Construction Environment verfügt, vereinfacht gesagt, über ein so genanntes Dictionary (assoziatives Array), welches u.a. die CC Variable mit dem zugeordneten Wert 'g++' beinhaltet. Somit kann man leicht nachschauen, welchen Wert die einzelnen Variablen haben. Geben wir doch einfach mal alle auf der Shell aus:

    #SConstruct
    env = Environment()
    
    #Einzelnes Element auswaehlen und ausgeben
    print "Used compiler: ", env['CC']
    
    dict = env.Dictionary() #Dictionary holen
    for i,j in dict.iteritems(): #Über alle Elemente iterieren
      print i,j		     #...und Key-Value Paar ausgeben
    

    Mit mehreren Construction Environments können wir auch mehrere Verhaltensmuster nachbilden, um z.B. einen Debug und einen Release - Vorgang zu erstellen:

    #SConstruct
    release = Environment(CCFLAGS = '-O2 -Os')
    debug = Environment(CCFLAGS = '-g')
    
    release.Program('foo', 'foo.cpp')
    debug.Program('bar', 'bar.cpp')
    

    8.2 Auf Bibliotheken/Header/Funktionen/typedefs testen
    *

    Was wollten wir eigentlich in diesem Abschnitt erreichen? Richtig: Prüfen auf Abhängigkeiten. Jetzt sind wir in der Lage, dies zu tun.
    Zuerst erstellen wir wie gehabt unsere Construction Environment. Dann verknüpfen wir einen Congfigure Context mit der Construction Environment, um die Tests durchzuführen und beenden den Vorgang schließlich mit der Finish() Methode:

    #SConstruct
    env = Environment()
    conf = Configure(env)
    
    #Auf Bibliothek pruefen
    if not conf.CheckLib('gdk'):
      print "Couldn't find gdk library. Exiting."
      Exit(1)
    
    #Auf C-Header pruefen
    if not conf.CheckCHeader('assert.h'):
      print "Couldn't find assert.h. Exiting."
      Exit(1)
    
    #Bei Vorhandensein von Header Flag mitgeben
    if conf.CheckCHeader('myHeader.h'):
      conf.env.Append('-DHAS_MY_HEADER_H')
    
    #Auf CPP-Header pruefen
    if not conf.CheckCXXHeader('list'):
      print "Couldn't find list. Exiting."
      Exit(1)
    
    #Auf Funktion testen
    if not conf.CheckFunc('memset'):
      print "Couldn't find memset. Exiting"
      Exit(1)
    
    #Auf typedef testen
    if not conf.CheckType('time_t', '#include <time.h>\n'):
      print "Couldn't find typedef time_t. Using long instead."
      conf.env.Append(CCFLAGS = '-Dtime_t=long')
    
    env.Program('main.cpp')
    
    env = conf.Finish()
    

    Es ist auch ohne weiteres möglich, benutzerdefinierte Tests zu bauen, aber dies würde den Rahmen sprengen, deshalb muss ich ein weiteres mal auf das Tutorial verweisen.

    **
    9 Zum Schluss
    **

    Das war jetzt nur eine kleine Vorstellung von dem, was SCons kann. Alles darzustellen war aber auch nicht mein Anspruch, deshalb sei an dieser Stelle nochmal auf das offizielle SCons-Tutorial verwiesen, welches sehr viele Informationen enthält und auf (fast) alle Fragen eine Antwort bietet.

    **
    10 What comes next...
    **

    Im nächsten Artikel wird uns Artchi dann bjam, das Build-System von boost, erklären.



  • Dann stürzt euch mal drauf...



  • GPC schrieb:

    Dann stürzt euch mal drauf...

    Ist das auf die Rechtschreibkorrekteure bezogen? 😕
    Oder ändert sich noch einmal was?



  • -predator- schrieb:

    GPC schrieb:

    Dann stürzt euch mal drauf...

    Ist das auf die Rechtschreibkorrekteure bezogen? 😕
    Oder ändert sich noch einmal was?

    Da oben wieder ein [R] steht, denke ich mal, dass es in die Korrektur soll. 🙂
    War ja auch so angekündigt.



  • estartu schrieb:

    -predator- schrieb:

    GPC schrieb:

    Dann stürzt euch mal drauf...

    Ist das auf die Rechtschreibkorrekteure bezogen? 😕
    Oder ändert sich noch einmal was?

    Da oben wieder ein [R] steht, denke ich mal, dass es in die Korrektur soll. 🙂
    War ja auch so angekündigt.

    Korrekt, die Rechtschreibprüfung sollte folgen, denn ich werde nichts mehr ändern. Thanks in advance.



  • Im nunmehr 3. Teil der Serie "Build-Systeme" werde ich dieses Mal SCons vorstellen.

    Inhalt:

    • 1 Hintergründe zu SCons
    • 2 Installation von SCons
    • 3 Die Datei "SConstruct"
    • 4 Erstellen von Programmen
    • 4.1 Keyword Arguments
    • 4.2 Mehrere Quelldateien
    • 5 Erstellen von Bibliotheken
    • 6 Einbinden von Bibliotheken
    • 7 Abhängigkeiten erkennen
    • 8 Auf Abhängigkeiten testen
    • 8.1 Construction Environments
    • 8.2 Auf Bibliotheken/Header/Funktionen/typedefs testen
    • 9 Zum Schluss
    • 10 What comes next...

    **
    1 Hintergründe zu SCons
    **

    SCons ist ein Open-Source Build-System, welches in Python implementiert wurde und seine Wurzeln im Perl-basierten Cons hat. Bei der Entwicklung von SCons wurde auf Korrektheit, Geschwindigkeit und besondere Einfachheit Wert gelegt. Wie Python selbst ist SCons sehr einfach zu benutzen, sowohl für einfache Aufgaben, als auch für komplizierte Builds.
    Um SCons zu benutzen braucht es nur minimale Python-Kenntnisse. Erwerben kann man diese mit dem Tutorial auf der Python-Homepage.

    SCons kann sowohl für C bzw. C++ als auch für Java verwendet werden, in diesem Artikel werde ich allerdings nur auf C++ eingehen und die Java-Anhänger auf das offizielle Tutorial verweisen.

    Da es von den Herstellern von SCons bereits ein sehr gutes Tutorial gibt, werde ich bloß auf die grundlegenden Funktionen eingehen und diese relativ schnell abhandeln.

    **
    2 Installation von SCons
    **

    SCons ist, da auf Python basierend, plattformunabhängig. Um mit SCons zu arbeiten, muss Python installiert sein.
    Man kann entweder die vorkompilierten Pakete installieren, oder es selbst kompilieren. Die unten erwähnten Pakete kann man in der "Download"-Section von http://www.scons.org/ finden.

    Für Windows gibt es, wie gewohnt, einen Installer, der wohl keiner weiterer Erklärung bedarf. 😉

    Auf RPM-basierten Systemen geht die Installation relativ simpel:

    rpm -uvh scons-x.x-x.noarch.rpm
    

    Auch bei Debian-basierten Systemen ist es nicht sehr schwer:

    apt-get install scons
    

    Wer SCons aus den Quellen übersetzen möchte, lädt sich den Tarball (oder das zip-File) herunter, entpackt ihn und wechselt ins scons Verzeichnis. Dort einfach

    python setup.py install
    

    eingeben und SCons wird in die üblichen Verzeichnisse ( /usr/local/ ) installiert. Dies erfordert allerdings Administrator-Rechte, besitzt man diese nicht, so kann man folgendermaßen ein anderes Verzeichnis angeben:

    python setup.py install --prefis=$HOME
    

    **
    3 Die Datei "SConstruct"
    **

    Diese Datei kann man in etwa mit einem Makefile oder einer Ant-XML-Datei vergleichen, denn in ihr stehen die Tasks und Targets, die SCons dann ausführt.
    Um die Aufgaben zu beschreiben, wird Python verwendet, d.h. dass diese Datei eigentlich ein Python-Skript ist. Im Unterschied zu Python-Skripten wird diese Datei aber nicht unbedingt geordnet abgearbeitet (siehe unten), sondern so, wie es die Erfüllung der Targets erfordert.
    Die "SConstruct" sollte man an der Wurzel im Projektverzeichnis abgelegen.

    **
    4 Erstellen von Programmen
    **

    Um zu zeigen, wie man mit SCons Programme erstellen kann, werden wir das allseits beliebte "Hallo Welt"-Beispiel verwenden:

    //hello_world.cpp
    #include <iostream>
    
    int main(int argc, char **argv) {
      std::cout<<"Hello World\n";
      return EXIT_SUCCESS;
    };
    

    Sehen wir uns die dazugehörige SConstruct-Datei an:

    #SConstruct
    #Kommentare beginnen in Python mit einem #-Zeichen
    Program("hello_world.cpp")
    

    Diese Anweisung sagt SCons zwei Dinge: Wir wollen (1) ein Programm erstellen, und zwar (2) aus der Datei "hello_world.cpp". Um den Build-Prozess zu starten, wechselt man in das Verzeichnis, in dem die Datei "SConstruct" liegt und tippt einfach

    scons
    

    in die Shell ein.
    Auf meinem Linux-System erscheinet folgende Ausgabe:

    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    

    Nun wurden eine hello_world.o und eine Binary namens hello_world erstellt. Auf Windows-Systemen sollten die Dateien hello_world.obj und hello_world.exe heißen.
    Es ist im Übrigen möglich, in einer SConstruct mehrere Targets zu definieren, also z.B. zwei oder drei Programme zu erstellen (wobei die Reihenfolge, wie SCons die Builds durchführt, nicht der Datei SConstruct folgen muss, d.h. es könnte sein, dass KillerApplication vor hello_world erstellt wird):

    Program("hello_world.cpp") #Erstes Target
    Program("KillerApplication.cpp") #Zweites Target
    

    Möchte man nur Objekt-Dateien erstellen, muss man das Target in der SConstruct leicht verändern:

    #SConstruct
    #Einfache Anführungszeichen gehen auch:
    Object('hello_world.cpp')
    

    Um den Effekt zu sehen, müssen wir zuerst die Ergebnisse des vorangegangenen Builds löschen. Dies geschieht mit einem Aufruf von scons und dem -c (oder --clean) Parameter:

    scons -c
    

    Führt man scons nun erneut aus, so wird nur eine hello_world.o bzw. hello_world.obj erstellt.

    Die Ausgaben, die SCons um die eigentlichen Befehle erzeugt, können leicht mit dem -Q Parameter unterdrückt werden, um nur die wirklich wichtigen Informationen zu sehen:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o hello_world.o hello_world.cpp
    

    Bisher hat SCons immer die Namen für die erzeugten Programme vergeben, das können wir aber auch selbst:

    #SConstruct
    Program("Hello", "hello_world.cpp")
    

    Aus hello_world.cpp wird nun eine Binary namens Hello erstellt:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -o Hello hello_world.o
    gpc@darkstar:~/tmp$ ls -l
    total 28
    -rwxr-xr-x  1 gpc users 12755 2005-12-18 16:38 Hello*
    -rw-r--r--  1 gpc users    36 2005-12-18 16:37 SConstruct
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:31 hello_world.o
    

    Man sieht, es wurde eine Hello erstellt.

    4.1 Keyword Arguments
    *

    Wenn man die Quelldateien zuerst auflisten möchte, und dann erst den Namen des Targets, so muss man mit so genannten "Keyword Arguments" arbeiten:

    #Standardreihenfolge
    Program(target = Hello, source = "hello_world.cpp")
    
    #Umgekehrte Reihenfolge
    Program(source = "hello_world.cpp", target = Hello)
    

    4.2 Mehrere Quelldateien
    *

    Größere Programme bestehen selten aus einer einzelnen Quelldatei, sollten sie jedenfalls nicht. SCons gibt uns selbstverständlich die Möglichkeit, mehrere Quelldateien zu kompilieren:

    #SConstruct
    #Nehmen wir an, es gibt noch eine goodbye.cpp
    Program("Hello", ["hello_world.cpp", "goodbye.cpp"])
    

    Ein Aufruf zeigt uns folgendes:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o goodbye.o goodbye.cpp
    g++ -o Hello hello_world.o goodbye.o
    gpc@darkstar:~/tmp$ ls -l
    total 32
    -rwxr-xr-x  1 gpc users 12799 2005-12-18 16:51 Hello*
    -rw-r--r--  1 gpc users    53 2005-12-18 16:51 SConstruct
    -rw-r--r--  1 gpc users     0 2005-12-18 16:51 goodbye.cpp
    -rw-r--r--  1 gpc users   625 2005-12-18 16:51 goodbye.o
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:40 hello_world.o
    

    Hier sieht man, dass die Datei goodbye.cpp erfolgreich kompiliert und in Hello gelinkt wurde.

    Dieses Konstrukt mit den eckigen Klammern in der SConstruct ist eine Python-Liste, welche die einzelnen Quelldateien beinhaltet. Es wäre auch möglich, nur einen Eintrag in die Liste zu setzen, dies hätte den gleichen Effekt wie vorher, als wir nur einen String mit dem Namen übergeben haben:

    #Beide Statements haben den gleichen Effekt
    Program("Main", ["main.cpp"])
    #Program("Main", "main.cpp")
    

    Da das Schreiben und Lesen einer längeren Liste nicht sehr viel Spaß macht, kann man alternativ auch die Python Split-Funktion aus dem string-Modul verwenden:

    #SConstruct
    Program("Calculator", Split("main.cpp calculator.cpp parser.cpp"))
    

    Auch möglich:

    #SConstruct
    fileList = Split("main.cpp calculator.cpp parser.cpp")
    Program("Calculator", fileList)
    

    Über mehrere Zeilen:

    #SConstruct
    fileList = Split("""main.cpp 
    		    calculator.cpp 
    		    parser.cpp""")
    Program("Calculator", fileList)
    

    Im letzten Beispiel wurde die "triple-quote"-Syntax verwendet, die es erlaubt, einen String über mehrere Zeilen hinweg zu definieren. Auch hier können es entweder drei doppelte oder drei einzelne Anführungszeichen sein.

    **
    5 Erstellen von Bibliotheken
    **

    Abgesehen von Programmen und Objektdateien kann SCons auch mit Leichtigkeit Bibliotheken erstellen, sowohl statische als auch dynamische.

    Erstellen wir eine kleine Bibliothek, die in je einer Quelldatei eine Grundrechenart bereitstellt (die Deklaration findet sich in den dazugehörigen *.hpp Dateien):

    #SConstruct
    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Library erstellt eine statische Bibltiothek
    Library(target="math", source=fileList)
    

    Um nun die "Bibliothek" zu erstellen, rufen wir scons auf, was folgende Ausgabe produziert:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o add.o add.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o mul.o div.o
    ranlib libmath.a
    ar: creating libmath.a
    

    Unsere Bibliothek wurde erfolgreich erstellt.

    Wer sich gerne exakt ausdrückt, der kann anstatt Library auch StaticLibrary schreiben, wenn er eine statische Bibliothek erstellen möchte. Ein Unterschied existiert nicht.

    Das Erstellen einer dynamischen Bibliothek (*.so auf POSIX-Systemen, *.dll auf Windows-Systemen) ist genauso einfach:

    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Nun erstellen wir eine dynamische Bibliothek
    SharedLibrary(target="math", source=fileList)
    

    Führen wir nun SCons aus:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -fPIC -c -o add.os add.cpp
    g++ -fPIC -c -o div.os div.cpp
    g++ -fPIC -c -o sub.os sub.cpp
    g++ -fPIC -c -o mul.os mul.cpp
    g++ -shared -o libmath.so add.os sub.os mul.os div.os
    

    Wie wir sehen, hat SCons eine dynamische Bibliothek erstellt, und das mit so wenig Aufwand.

    **
    6 Einbinden von Bibliotheken
    **

    Natürlich wäre die beste Bibliothek nutzlos, wenn man sie nicht in seinen Programmen verwenden würde. Damit dies möglich ist, muss man die Bibliothek natürlich in die Executable hineinlinken.

    Die main.cpp sieht so aus:

    #include <iostream>
    #include "add.hpp"
    
    int main(int argc, char **argv) {
      int result = add(2,7);
      std::cout<<result<<'\n';
      return EXIT_SUCCESS;
    };
    

    Die SConstruct so:

    #SConstruct
    Library("math", ["add.cpp", "sub.cpp", "div.cpp", "mul.cpp"])
    Program("main.cpp", LIBS=["math"], LIBPATH=".")
    

    Bemerkenswert ist, dass wir den Namen der Bibliothek einfach so angeben können, ohne uns über systemspezifische Präfixe Gedanken machen zu müssen.

    Wenn wir scons jetzt aufrufen, erhalten wir folgende Ausgabe:

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o add.o add.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o div.o mul.o
    ranlib libmath.a
    ar: creating libmath.a
    g++ -c -o main.o main.cpp
    g++ -o main main.o -L. -lmath
    scons: done building targets.
    

    Über das oben verwendete LIBPATH Argument sucht SCons selbstständig nach Bibliotheken in den angegebenen Verzeichnissen:

    #SConstruct
    Program("Test", LIBS = "someLib", LIBPATH = ['/usr/lib', '/usr/local/lib'])
    

    SCons sucht nun in den beiden Verzeichnissen, ob es eine "someLib" Bibliothek findet. Kann keine Bibliothek gefunden werden, meldet sich SCons mit einer Fehlermeldung.

    **
    7 Abhängigkeiten erkennen
    **

    Kommen wir zu einem interessanteren Feature: der Erkennung von Abhängigkeiten. Wenn sich an unserem Programm etwas geändert hat und wir es neu kompilieren, dann wollen wir, dass nur das nötigste neu übersetzt wird und Teile, die sich nicht geändert haben, unangetastet bleiben.

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    
    gpc@darkstar:~/tmp$ scons -Q
    scons: `.' is up to date.
    

    SCons hat erkannt, dass seit dem letzten Build keine Änderungen stattgefunden haben und hat gar nichts gemacht. Soweit so gut.
    Standardmäßig erkennt SCons eine Änderung am Inhalt der Datei, nicht am Datum des letzten Zugriffs (wie make es macht).

    Will man das Verhalten von make, muss man die SourceSignatures Funktion benutzen:

    #SConstruct
    Program("hello_world.cpp")
    
    #Standardverhalten:
    #SourceSignatures("MD5")
    
    #make-Verhalten:
    SourceSignatures("timestamp")
    

    Sofern ein Target von einem anderen abhängt (z.B. Programm von Bibliothek), kann man auch hier festlegen, anhand welcher Kriterien SCons über einen Rebuild entscheidet. Dies geschieht mittels der TargetSignatures Funktion.
    Das Standardverhalten ist, dass SCons anhand einer Target-Signatur, die aus den Signaturen der Quelldateien besteht, entscheidet ob ein Rebuild notwendig ist:

    Program("hello_world.cpp")
    TargetSignatures("build")
    

    Wenn sich eine Quelldatei aber so verändert hat, dass der Inhalt des Target der gleiche bleibt, dann ist kein kompletter Rebuild notwendig. Dieses Verhalten kann man so erreichen:

    Program("hello_world.cpp")
    TargetSignatures("content")
    

    Nun ist es gängige Praxis, die Schnittstelle einer Klasse in eine Header-Datei zu schreiben und die Implementation in eine Source-Datei auszulagern. Sehen wir uns folgendes kleines Programm an:

    //foo.hpp
    #ifndef FOO_HPP
    #define FOO_HPP
    
    struct Foo {
      void bar() const;
    };
    
    #endif
    
    //foo.cpp
    #include "foo.hpp"
    #include <iostream>
    
    void Foo::bar() const {
      std::cout<<"Blub blub\n";
    };
    
    //hello_world.cpp
    #include "foo.hpp"
    
    int main() {
      Foo f;
      f.bar();
      return 0;
    };
    

    Wenn sich an foo.hpp etwas ändert, will ich, dass SCons das erkennt und einen Rebuild durchführt, und das sage ich SCons mit der CPPPATH Variable:

    #SConstruct
    Program(['hello_world.cpp','foo.cpp'], CPPPATH = '.')
    

    Nun sieht SCons im aktuellen (".") Verzeichnis nach, um herauszufinden ob ein Rebuild notwendig ist.
    Analog zu der LIBPATH Variable, kann die CPPPATH Variable eine Liste sein:

    #SConstruct
    Program('hello_world.cpp', CPPPATH = ['.', '/home/gpc/tmp/include'])
    

    **
    8 Auf Abhängigkeiten testen
    **

    Viele Programme benutzen externe Bibliotheken, um fehlende Funktionalität einer Standardbibliothek auszugleichen. Wenn der Benutzer unser Programm benutzen will, dann braucht er ebenfalls diese Bibliothek, um das Programm erfolgreich kompilieren zu können. Hat er die Bibliothek nicht installiert, wird der Kompiliervorgang abbrechen, da der Compiler die Abhängigkeiten nicht auflösen kann. Nicht sehr schön. Eine elegantere Lösung wäre doch, vorher abzufragen, ob Bibliothek Foo existiert und bei Nichtvorhandensein eine entsprechende Fehlermeldung auszugeben (bei den autotools wird dies von autoconf erledigt). Um dies zu bewerkstelligen, müssen wir zuerst einen kleinen Abstecher zum Thema Construction Environments machen:

    8.1 Construction Environments
    *

    Eine so genannte Construction Environment gibt uns die Möglichkeit zu bestimmen, wie unsere Programme erstellt werden, welcher Compiler genutzt wird oder welche Flags gesetzt werden.

    Mit folgendem Python-Code kann man eine Construction Environment erstellen:

    #SConstruct
    env = Environment()
    

    Hierbei wird die Construction Environment 'env' mit den auf dem System installierten Tools initialisiert, also z.B. wird bei installiertem g++ der CC-Wert auf g++ gesetzt, ebenso sieht es mit dem Linker und den anderen, für den Build-Prozess benötigten Daten aus.
    Aber es ist halt immer das gleiche mit Default-Werten: Meistens wollen wir spezielle Parameter übergeben. Nun, das ist ein leichtes:

    #SConstruct
    env = Environment(CC = 'gcc', CCFLAGS = '-O2 -g')
    
    #Ersetzen von gcc durch g++, ist ja ein cpp file
    env.Replace(CC = 'g++')
    
    #Anhaengen von -W Option
    #Wuerde CCFLAGS noch nicht existieren, wuerde es angelegt werden
    env.Append(CCFLAGS = ' -W')
    
    #Alternativ kann man Prepend zum Voranstellen benutzen:
    env.Prepend(CCFLAGS = ' -Weffc++ ')
    
    #Achtung: Neuer Aufruf:
    env.Program('main.cpp')
    

    An der Ausgabe erkennen wir, dass scons unsere Angaben übernommen hat:

    bash-3.00$ scons -Q
    g++ -Weffc++ -O2 -g -W -c -o main.o main.cpp
    g++ -o main main.o
    

    Eine Construction Environment verfügt, vereinfacht gesagt, über ein so genanntes Dictionary (assoziatives Array), welches u.a. die CC-Variable mit dem zugeordneten Wert 'g++' beinhaltet. Somit kann man leicht nachschauen, welchen Wert die einzelnen Variablen haben. Geben wir doch einfach mal alle auf der Shell aus:

    #SConstruct
    env = Environment()
    
    #Einzelnes Element auswaehlen und ausgeben
    print "Used compiler: ", env['CC']
    
    dict = env.Dictionary() #Dictionary holen
    for i,j in dict.iteritems(): #Über alle Elemente iterieren
      print i,j		     #...und Key-Value Paar ausgeben
    

    Mit mehreren Construction Environments können wir auch mehrere Verhaltensmuster nachbilden, um z.B. einen Debug- und einen Release-Vorgang zu erstellen:

    #SConstruct
    release = Environment(CCFLAGS = '-O2 -Os')
    debug = Environment(CCFLAGS = '-g')
    
    release.Program('foo', 'foo.cpp')
    debug.Program('bar', 'bar.cpp')
    

    8.2 Auf Bibliotheken/Header/Funktionen/typedefs testen
    *

    Was wollten wir eigentlich in diesem Abschnitt erreichen? Richtig: Prüfen auf Abhängigkeiten. Jetzt sind wir in der Lage, dies zu tun.
    Zuerst erstellen wir wie gehabt unsere Construction Environment. Dann verknüpfen wir einen Configure Context mit der Construction Environment, um die Tests durchzuführen und beenden den Vorgang schließlich mit der Finish()-Methode:

    #SConstruct
    env = Environment()
    conf = Configure(env)
    
    #Auf Bibliothek pruefen
    if not conf.CheckLib('gdk'):
      print "Couldn't find gdk library. Exiting."
      Exit(1)
    
    #Auf C-Header pruefen
    if not conf.CheckCHeader('assert.h'):
      print "Couldn't find assert.h. Exiting."
      Exit(1)
    
    #Bei Vorhandensein von Header Flag mitgeben
    if conf.CheckCHeader('myHeader.h'):
      conf.env.Append('-DHAS_MY_HEADER_H')
    
    #Auf CPP-Header pruefen
    if not conf.CheckCXXHeader('list'):
      print "Couldn't find list. Exiting."
      Exit(1)
    
    #Auf Funktion testen
    if not conf.CheckFunc('memset'):
      print "Couldn't find memset. Exiting"
      Exit(1)
    
    #Auf typedef testen
    if not conf.CheckType('time_t', '#include <time.h>\n'):
      print "Couldn't find typedef time_t. Using long instead."
      conf.env.Append(CCFLAGS = '-Dtime_t=long')
    
    env.Program('main.cpp')
    
    env = conf.Finish()
    

    Es ist auch ohne weiteres möglich, benutzerdefinierte Tests zu bauen, aber dies würde den Rahmen sprengen, deshalb muss ich ein weiteres Mal auf das Tutorial verweisen.

    **
    9 Zum Schluss
    **

    Das war jetzt nur eine kleine Vorstellung von dem, was SCons kann. Alles darzustellen war aber auch nicht mein Anspruch, deshalb sei an dieser Stelle noch mal auf das offizielle SCons-Tutorial verwiesen, welches sehr viele Informationen enthält und auf (fast) alle Fragen eine Antwort bietet.

    **
    10 What comes next...
    **

    Im nächsten Artikel wird uns Artchi dann bjam, das Build-System von boost, erklären.



  • Im nunmehr 3. Teil der Serie "Build-Systeme" werde ich dieses Mal SCons vorstellen.

    Inhalt:

    • 1 Hintergründe zu SCons
    • 2 Installation von SCons
    • 3 Die Datei "SConstruct"
    • 4 Erstellen von Programmen
    • 4.1 Keyword Arguments
    • 4.2 Mehrere Quelldateien
    • 5 Erstellen von Bibliotheken
    • 6 Einbinden von Bibliotheken
    • 7 Abhängigkeiten erkennen
    • 8 Auf Abhängigkeiten testen
    • 8.1 Construction Environments
    • 8.2 Auf Bibliotheken/Header/Funktionen/typedefs testen
    • 9 Zum Schluss
    • 10 What comes next...

    **
    1 Hintergründe zu SCons
    **

    SCons ist ein Open-Source Build-System, welches in Python implementiert wurde und seine Wurzeln im Perl-basierten Cons hat. Bei der Entwicklung von SCons wurde auf Korrektheit, Geschwindigkeit und besondere Einfachheit Wert gelegt. Wie Python selbst ist SCons sehr einfach zu benutzen, sowohl für einfache Aufgaben, als auch für komplizierte Builds.
    Um SCons zu benutzen braucht es nur minimale Python-Kenntnisse. Erwerben kann man diese mit dem Tutorial auf der Python-Homepage.

    SCons kann sowohl für C bzw. C++ als auch für Java verwendet werden, in diesem Artikel werde ich allerdings nur auf C++ eingehen und die Java-Anhänger auf das offizielle Tutorial verweisen.

    Da es von den Herstellern von SCons bereits ein sehr gutes Tutorial gibt, werde ich bloß auf die grundlegenden Funktionen eingehen und diese relativ schnell abhandeln.

    **
    2 Installation von SCons
    **

    SCons ist, da auf Python basierend, plattformunabhängig. Um mit SCons zu arbeiten, muss Python installiert sein.
    Man kann entweder die vorkompilierten Pakete installieren, oder es selbst kompilieren. Die unten erwähnten Pakete kann man in der "Download"-Section von http://www.scons.org/ finden.

    Für Windows gibt es, wie gewohnt, einen Installer, der wohl keiner weiterer Erklärung bedarf. 😉

    Auf RPM-basierten Systemen geht die Installation relativ simpel:

    rpm -uvh scons-x.x-x.noarch.rpm
    

    Auch bei Debian-basierten Systemen ist es nicht sehr schwer:

    apt-get install scons
    

    Wer SCons aus den Quellen übersetzen möchte, lädt sich den Tarball (oder das zip-File) herunter, entpackt ihn und wechselt ins scons Verzeichnis. Dort einfach

    python setup.py install
    

    eingeben und SCons wird in die üblichen Verzeichnisse ( /usr/local/ ) installiert. Dies erfordert allerdings Administrator-Rechte, besitzt man diese nicht, so kann man folgendermaßen ein anderes Verzeichnis angeben:

    python setup.py install --prefis=$HOME
    

    **
    3 Die Datei "SConstruct"
    **

    Diese Datei kann man in etwa mit einem Makefile oder einer Ant-XML-Datei vergleichen, denn in ihr stehen die Tasks und Targets, die SCons dann ausführt.
    Um die Aufgaben zu beschreiben, wird Python verwendet, d.h. dass diese Datei eigentlich ein Python-Skript ist. Im Unterschied zu Python-Skripten wird diese Datei aber nicht unbedingt geordnet abgearbeitet (siehe unten), sondern so, wie es die Erfüllung der Targets erfordert.
    Die "SConstruct" sollte man an der Wurzel im Projektverzeichnis abgelegen.

    **
    4 Erstellen von Programmen
    **

    Um zu zeigen, wie man mit SCons Programme erstellen kann, werden wir das allseits beliebte "Hallo Welt"-Beispiel verwenden:

    //hello_world.cpp
    #include <iostream>
    
    int main(int argc, char **argv) {
      std::cout<<"Hello World\n";
      return EXIT_SUCCESS;
    };
    

    Sehen wir uns die dazugehörige SConstruct-Datei an:

    #SConstruct
    #Kommentare beginnen in Python mit einem #-Zeichen
    Program("hello_world.cpp")
    

    Diese Anweisung sagt SCons zwei Dinge: Wir wollen (1) ein Programm erstellen, und zwar (2) aus der Datei "hello_world.cpp". Um den Build-Prozess zu starten, wechselt man in das Verzeichnis, in dem die Datei "SConstruct" liegt und tippt einfach

    scons
    

    in die Shell ein.
    Auf meinem Linux-System erscheinet folgende Ausgabe:

    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    

    Nun wurden eine hello_world.o und eine Binary namens hello_world erstellt. Auf Windows-Systemen sollten die Dateien hello_world.obj und hello_world.exe heißen.
    Es ist im Übrigen möglich, in einer SConstruct mehrere Targets zu definieren, also z.B. zwei oder drei Programme zu erstellen (wobei die Reihenfolge, wie SCons die Builds durchführt, nicht der Datei SConstruct folgen muss, d.h. es könnte sein, dass KillerApplication vor hello_world erstellt wird):

    Program("hello_world.cpp") #Erstes Target
    Program("KillerApplication.cpp") #Zweites Target
    

    Möchte man nur Objekt-Dateien erstellen, muss man das Target in der SConstruct leicht verändern:

    #SConstruct
    #Einfache Anführungszeichen gehen auch:
    Object('hello_world.cpp')
    

    Um den Effekt zu sehen, müssen wir zuerst die Ergebnisse des vorangegangenen Builds löschen. Dies geschieht mit einem Aufruf von scons und dem -c (oder --clean) Parameter:

    scons -c
    

    Führt man scons nun erneut aus, so wird nur eine hello_world.o bzw. hello_world.obj erstellt.

    Die Ausgaben, die SCons um die eigentlichen Befehle erzeugt, können leicht mit dem -Q Parameter unterdrückt werden, um nur die wirklich wichtigen Informationen zu sehen:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o hello_world.o hello_world.cpp
    

    Bisher hat SCons immer die Namen für die erzeugten Programme vergeben, das können wir aber auch selbst:

    #SConstruct
    Program("Hello", "hello_world.cpp")
    

    Aus hello_world.cpp wird nun eine Binary namens Hello erstellt:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -o Hello hello_world.o
    gpc@darkstar:~/tmp$ ls -l
    total 28
    -rwxr-xr-x  1 gpc users 12755 2005-12-18 16:38 Hello*
    -rw-r--r--  1 gpc users    36 2005-12-18 16:37 SConstruct
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:31 hello_world.o
    

    Man sieht, es wurde eine Hello erstellt.

    4.1 Keyword Arguments
    *

    Wenn man die Quelldateien zuerst auflisten möchte, und dann erst den Namen des Targets, so muss man mit so genannten "Keyword Arguments" arbeiten:

    #Standardreihenfolge
    Program(target = Hello, source = "hello_world.cpp")
    
    #Umgekehrte Reihenfolge
    Program(source = "hello_world.cpp", target = Hello)
    

    4.2 Mehrere Quelldateien
    *

    Größere Programme bestehen selten aus einer einzelnen Quelldatei, sollten sie jedenfalls nicht. SCons gibt uns selbstverständlich die Möglichkeit, mehrere Quelldateien zu kompilieren:

    #SConstruct
    #Nehmen wir an, es gibt noch eine goodbye.cpp
    Program("Hello", ["hello_world.cpp", "goodbye.cpp"])
    

    Ein Aufruf zeigt uns folgendes:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o goodbye.o goodbye.cpp
    g++ -o Hello hello_world.o goodbye.o
    gpc@darkstar:~/tmp$ ls -l
    total 32
    -rwxr-xr-x  1 gpc users 12799 2005-12-18 16:51 Hello*
    -rw-r--r--  1 gpc users    53 2005-12-18 16:51 SConstruct
    -rw-r--r--  1 gpc users     0 2005-12-18 16:51 goodbye.cpp
    -rw-r--r--  1 gpc users   625 2005-12-18 16:51 goodbye.o
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:40 hello_world.o
    

    Hier sieht man, dass die Datei goodbye.cpp erfolgreich kompiliert und in Hello gelinkt wurde.

    Dieses Konstrukt mit den eckigen Klammern in der SConstruct ist eine Python-Liste, welche die einzelnen Quelldateien beinhaltet. Es wäre auch möglich, nur einen Eintrag in die Liste zu setzen, dies hätte den gleichen Effekt wie vorher, als wir nur einen String mit dem Namen übergeben haben:

    #Beide Statements haben den gleichen Effekt
    Program("Main", ["main.cpp"])
    #Program("Main", "main.cpp")
    

    Da das Schreiben und Lesen einer längeren Liste nicht sehr viel Spaß macht, kann man alternativ auch die Python Split-Funktion aus dem string-Modul verwenden:

    #SConstruct
    Program("Calculator", Split("main.cpp calculator.cpp parser.cpp"))
    

    Auch möglich:

    #SConstruct
    fileList = Split("main.cpp calculator.cpp parser.cpp")
    Program("Calculator", fileList)
    

    Über mehrere Zeilen:

    #SConstruct
    fileList = Split("""main.cpp 
    		    calculator.cpp 
    		    parser.cpp""")
    Program("Calculator", fileList)
    

    Im letzten Beispiel wurde die "triple-quote"-Syntax verwendet, die es erlaubt, einen String über mehrere Zeilen hinweg zu definieren. Auch hier können es entweder drei doppelte oder drei einzelne Anführungszeichen sein.

    **
    5 Erstellen von Bibliotheken
    **

    Abgesehen von Programmen und Objektdateien kann SCons auch mit Leichtigkeit Bibliotheken erstellen, sowohl statische als auch dynamische.

    Erstellen wir eine kleine Bibliothek, die in je einer Quelldatei eine Grundrechenart bereitstellt (die Deklaration findet sich in den dazugehörigen *.hpp Dateien):

    #SConstruct
    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Library erstellt eine statische Bibltiothek
    Library(target="math", source=fileList)
    

    Um nun die "Bibliothek" zu erstellen, rufen wir scons auf, was folgende Ausgabe produziert:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o add.o add.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o mul.o div.o
    ranlib libmath.a
    ar: creating libmath.a
    

    Unsere Bibliothek wurde erfolgreich erstellt.

    Wer sich gerne exakt ausdrückt, der kann anstatt Library auch StaticLibrary schreiben, wenn er eine statische Bibliothek erstellen möchte. Ein Unterschied existiert nicht.

    Das Erstellen einer dynamischen Bibliothek (*.so auf POSIX-Systemen, *.dll auf Windows-Systemen) ist genauso einfach:

    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Nun erstellen wir eine dynamische Bibliothek
    SharedLibrary(target="math", source=fileList)
    

    Führen wir nun SCons aus:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -fPIC -c -o add.os add.cpp
    g++ -fPIC -c -o div.os div.cpp
    g++ -fPIC -c -o sub.os sub.cpp
    g++ -fPIC -c -o mul.os mul.cpp
    g++ -shared -o libmath.so add.os sub.os mul.os div.os
    

    Wie wir sehen, hat SCons eine dynamische Bibliothek erstellt, und das mit so wenig Aufwand.

    **
    6 Einbinden von Bibliotheken
    **

    Natürlich wäre die beste Bibliothek nutzlos, wenn man sie nicht in seinen Programmen verwenden würde. Damit dies möglich ist, muss man die Bibliothek natürlich in die Executable hineinlinken.

    Die main.cpp sieht so aus:

    #include <iostream>
    #include "add.hpp"
    
    int main(int argc, char **argv) {
      int result = add(2,7);
      std::cout<<result<<'\n';
      return EXIT_SUCCESS;
    };
    

    Die SConstruct so:

    #SConstruct
    Library("math", ["add.cpp", "sub.cpp", "div.cpp", "mul.cpp"])
    Program("main.cpp", LIBS=["math"], LIBPATH=".")
    

    Bemerkenswert ist, dass wir den Namen der Bibliothek einfach so angeben können, ohne uns über systemspezifische Präfixe Gedanken machen zu müssen.

    Wenn wir scons jetzt aufrufen, erhalten wir folgende Ausgabe:

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o add.o add.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o div.o mul.o
    ranlib libmath.a
    ar: creating libmath.a
    g++ -c -o main.o main.cpp
    g++ -o main main.o -L. -lmath
    scons: done building targets.
    

    Über das oben verwendete LIBPATH Argument sucht SCons selbstständig nach Bibliotheken in den angegebenen Verzeichnissen:

    #SConstruct
    Program("Test", LIBS = "someLib", LIBPATH = ['/usr/lib', '/usr/local/lib'])
    

    SCons sucht nun in den beiden Verzeichnissen, ob es eine "someLib" Bibliothek findet. Kann keine Bibliothek gefunden werden, meldet sich SCons mit einer Fehlermeldung.

    **
    7 Abhängigkeiten erkennen
    **

    Kommen wir zu einem interessanteren Feature: der Erkennung von Abhängigkeiten. Wenn sich an unserem Programm etwas geändert hat und wir es neu kompilieren, dann wollen wir, dass nur das nötigste neu übersetzt wird und Teile, die sich nicht geändert haben, unangetastet bleiben.

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    
    gpc@darkstar:~/tmp$ scons -Q
    scons: `.' is up to date.
    

    SCons hat erkannt, dass seit dem letzten Build keine Änderungen stattgefunden haben und hat gar nichts gemacht. Soweit so gut.
    Standardmäßig erkennt SCons eine Änderung am Inhalt der Datei, nicht am Datum des letzten Zugriffs (wie make es macht).

    Will man das Verhalten von make, muss man die SourceSignatures Funktion benutzen:

    #SConstruct
    Program("hello_world.cpp")
    
    #Standardverhalten:
    #SourceSignatures("MD5")
    
    #make-Verhalten:
    SourceSignatures("timestamp")
    

    Sofern ein Target von einem anderen abhängt (z.B. Programm von Bibliothek), kann man auch hier festlegen, anhand welcher Kriterien SCons über einen Rebuild entscheidet. Dies geschieht mittels der TargetSignatures Funktion.
    Das Standardverhalten ist, dass SCons anhand einer Target-Signatur, die aus den Signaturen der Quelldateien besteht, entscheidet ob ein Rebuild notwendig ist:

    Program("hello_world.cpp")
    TargetSignatures("build")
    

    Wenn sich eine Quelldatei aber so verändert hat, dass der Inhalt des Target der gleiche bleibt, dann ist kein kompletter Rebuild notwendig. Dieses Verhalten kann man so erreichen:

    Program("hello_world.cpp")
    TargetSignatures("content")
    

    Nun ist es gängige Praxis, die Schnittstelle einer Klasse in eine Header-Datei zu schreiben und die Implementation in eine Source-Datei auszulagern. Sehen wir uns folgendes kleines Programm an:

    //foo.hpp
    #ifndef FOO_HPP
    #define FOO_HPP
    
    struct Foo {
      void bar() const;
    };
    
    #endif
    
    //foo.cpp
    #include "foo.hpp"
    #include <iostream>
    
    void Foo::bar() const {
      std::cout<<"Blub blub\n";
    };
    
    //hello_world.cpp
    #include "foo.hpp"
    
    int main() {
      Foo f;
      f.bar();
      return 0;
    };
    

    Wenn sich an foo.hpp etwas ändert, will ich, dass SCons das erkennt und einen Rebuild durchführt, und das sage ich SCons mit der CPPPATH Variable:

    #SConstruct
    Program(['hello_world.cpp','foo.cpp'], CPPPATH = '.')
    

    Nun sieht SCons im aktuellen (".") Verzeichnis nach, um herauszufinden ob ein Rebuild notwendig ist.
    Analog zu der LIBPATH Variable, kann die CPPPATH Variable eine Liste sein:

    #SConstruct
    Program('hello_world.cpp', CPPPATH = ['.', '/home/gpc/tmp/include'])
    

    **
    8 Auf Abhängigkeiten testen
    **

    Viele Programme benutzen externe Bibliotheken, um fehlende Funktionalität einer Standardbibliothek auszugleichen. Wenn der Benutzer unser Programm benutzen will, dann braucht er ebenfalls diese Bibliothek, um das Programm erfolgreich kompilieren zu können. Hat er die Bibliothek nicht installiert, wird der Kompiliervorgang abbrechen, da der Compiler die Abhängigkeiten nicht auflösen kann. Nicht sehr schön. Eine elegantere Lösung wäre doch, vorher abzufragen, ob Bibliothek Foo existiert und bei Nichtvorhandensein eine entsprechende Fehlermeldung auszugeben (bei den autotools wird dies von autoconf erledigt). Um dies zu bewerkstelligen, müssen wir zuerst einen kleinen Abstecher zum Thema Construction Environments machen:

    8.1 Construction Environments
    *

    Eine so genannte Construction Environment gibt uns die Möglichkeit zu bestimmen, wie unsere Programme erstellt werden, welcher Compiler genutzt wird oder welche Flags gesetzt werden.

    Mit folgendem Python-Code kann man eine Construction Environment erstellen:

    #SConstruct
    env = Environment()
    

    Hierbei wird die Construction Environment 'env' mit den auf dem System installierten Tools initialisiert, also z.B. wird bei installiertem g++ der CC-Wert auf g++ gesetzt, ebenso sieht es mit dem Linker und den anderen, für den Build-Prozess benötigten Daten aus.
    Aber es ist halt immer das gleiche mit Default-Werten: Meistens wollen wir spezielle Parameter übergeben. Nun, das ist ein leichtes:

    #SConstruct
    env = Environment(CC = 'gcc', CCFLAGS = '-O2 -g')
    
    #Ersetzen von gcc durch g++, ist ja ein cpp file
    env.Replace(CC = 'g++')
    
    #Anhaengen von -W Option
    #Wuerde CCFLAGS noch nicht existieren, wuerde es angelegt werden
    env.Append(CCFLAGS = ' -W')
    
    #Alternativ kann man Prepend zum Voranstellen benutzen:
    env.Prepend(CCFLAGS = ' -Weffc++ ')
    
    #Achtung: Neuer Aufruf:
    env.Program('main.cpp')
    

    An der Ausgabe erkennen wir, dass scons unsere Angaben übernommen hat:

    bash-3.00$ scons -Q
    g++ -Weffc++ -O2 -g -W -c -o main.o main.cpp
    g++ -o main main.o
    

    Eine Construction Environment verfügt, vereinfacht gesagt, über ein so genanntes Dictionary (assoziatives Array), welches u.a. die CC-Variable mit dem zugeordneten Wert 'g++' beinhaltet. Somit kann man leicht nachschauen, welchen Wert die einzelnen Variablen haben. Geben wir doch einfach mal alle auf der Shell aus:

    #SConstruct
    env = Environment()
    
    #Einzelnes Element auswaehlen und ausgeben
    print "Used compiler: ", env['CC']
    
    dict = env.Dictionary() #Dictionary holen
    for i,j in dict.iteritems(): #Über alle Elemente iterieren
      print i,j		     #...und Key-Value Paar ausgeben
    

    Mit mehreren Construction Environments können wir auch mehrere Verhaltensmuster nachbilden, um z.B. einen Debug- und einen Release-Vorgang zu erstellen:

    #SConstruct
    release = Environment(CCFLAGS = '-O2 -Os')
    debug = Environment(CCFLAGS = '-g')
    
    release.Program('foo', 'foo.cpp')
    debug.Program('bar', 'bar.cpp')
    

    8.2 Auf Bibliotheken/Header/Funktionen/typedefs testen
    *

    Was wollten wir eigentlich in diesem Abschnitt erreichen? Richtig: Prüfen auf Abhängigkeiten. Jetzt sind wir in der Lage, dies zu tun.
    Zuerst erstellen wir wie gehabt unsere Construction Environment. Dann verknüpfen wir einen Configure Context mit der Construction Environment, um die Tests durchzuführen und beenden den Vorgang schließlich mit der Finish()-Methode:

    #SConstruct
    env = Environment()
    conf = Configure(env)
    
    #Auf Bibliothek pruefen
    if not conf.CheckLib('gdk'):
      print "Couldn't find gdk library. Exiting."
      Exit(1)
    
    #Auf C-Header pruefen
    if not conf.CheckCHeader('assert.h'):
      print "Couldn't find assert.h. Exiting."
      Exit(1)
    
    #Bei Vorhandensein von Header Flag mitgeben
    if conf.CheckCHeader('myHeader.h'):
      conf.env.Append('-DHAS_MY_HEADER_H')
    
    #Auf CPP-Header pruefen
    if not conf.CheckCXXHeader('list'):
      print "Couldn't find list. Exiting."
      Exit(1)
    
    #Auf Funktion testen
    if not conf.CheckFunc('memset'):
      print "Couldn't find memset. Exiting"
      Exit(1)
    
    #Auf typedef testen
    if not conf.CheckType('time_t', '#include <time.h>\n'):
      print "Couldn't find typedef time_t. Using long instead."
      conf.env.Append(CCFLAGS = '-Dtime_t=long')
    
    env.Program('main.cpp')
    
    env = conf.Finish()
    

    Es ist auch ohne weiteres möglich, benutzerdefinierte Tests zu bauen, aber dies würde den Rahmen sprengen, deshalb muss ich ein weiteres Mal auf das Tutorial verweisen.

    **
    9 Zum Schluss
    **

    Das war jetzt nur eine kleine Vorstellung von dem, was SCons kann. Alles darzustellen war aber auch nicht mein Anspruch, deshalb sei an dieser Stelle noch mal auf das offizielle SCons-Tutorial verwiesen, welches sehr viele Informationen enthält und auf (fast) alle Fragen eine Antwort bietet.

    **
    10 What comes next...
    **

    Im nächsten Artikel wird uns Artchi dann bjam, das Build-System von boost, erklären.



  • @predator
    Danke für die Korrektur.

    Btw. Irgendwie hatt ich Probleme mit der StripTags.exe, hab den Text von predator in die Zwischenablage kopiert, StripTags.exe ausgeführt und wieder einfügen wollen, war aber nix da zum Einfügen 😕



  • Das ist komisch, hast du es nochmal versucht?

    Wenn es bis Montag nicht geklappt hat, mach ich dir das. 🙂
    Blos vorher hab ich nicht die Ruhe.



  • estartu schrieb:

    Das ist komisch, hast du es nochmal versucht?

    Jo, gleiches Problem. Versteh nicht warum. Werd mich mal mit dem Source befassen, wahrscheinlich krieg ich dann raus woran's lag.

    Wenn es bis Montag nicht geklappt hat, mach ich dir das. 🙂

    Danke, aber ich hab n Python Skript dass diese Aufgabe bereits zuverlässig erledigt hat.

    Würdest du noch in SideWinders Ant-Artikel den "What comes next?" Abschnitt anpassen (oder es ihm sagen)? Da steht nämlich immer noch drin, dass Talla der nächste Artikel sei, aber jetzt kommt ja erst meiner. Bin mir eh nicht so sicher, welcher als nächster kommt, Artchis oder Tallas. Mal mit den beiden reden...



  • Im nunmehr dritten Teil der Serie "Build-Systeme" werde ich dieses Mal SCons vorstellen.

    Inhalt:

    • 1 Hintergründe zu SCons
    • 2 Installation von SCons
    • 3 Die Datei "SConstruct"
    • 4 Erstellen von Programmen
    • 4.1 Keyword Arguments
    • 4.2 Mehrere Quelldateien
    • 5 Erstellen von Bibliotheken
    • 6 Einbinden von Bibliotheken
    • 7 Abhängigkeiten erkennen
    • 8 Auf Abhängigkeiten testen
    • 8.1 Construction Environments
    • 8.2 Auf Bibliotheken/Header/Funktionen/typedefs testen
    • 9 Zum Schluss
    • 10 What comes next...

    **
    1 Hintergründe zu SCons
    **

    SCons ist ein Open Source Build-System, welches in Python implementiert wurde und seine Wurzeln im auf Perl basierenden Cons hat. Bei der Entwicklung von SCons wurde auf Korrektheit, Geschwindigkeit und besondere Einfachheit Wert gelegt. Wie Python selbst ist SCons sehr einfach zu benutzen, sowohl für einfache Aufgaben, als auch für komplizierte Builds.
    Um SCons zu benutzen***,*** braucht es nur minimale Python-Kenntnisse. Erwerben kann man diese mit dem Tutorial auf der Python-Homepage.

    SCons kann sowohl für C bzw. C++ als auch für Java verwendet werden***. In*** diesem Artikel werde ich allerdings nur auf C++ eingehen und die Java-Anhänger auf das offizielle Tutorial verweisen.

    Da es von den Herstellern von SCons bereits ein sehr gutes Tutorial gibt, werde ich bloß auf die grundlegenden Funktionen eingehen und diese relativ schnell abhandeln.

    **
    2 Installation von SCons
    **

    SCons ist, da auf Python basierend, plattformunabhängig. Um mit SCons zu arbeiten, muss Python installiert sein.
    Man kann entweder die vorkompilierten Pakete installieren oder es selbst kompilieren. Die unten erwähnten Pakete kann man in der Download-Section von http://www.scons.org/ finden.

    Für Windows gibt es, wie gewohnt, einen Installer, der wohl keiner weiteren Erklärung bedarf. 😉

    Auf auf RPM basierenden Systemen geht die Installation relativ simpel:

    rpm -uvh scons-x.x-x.noarch.rpm
    

    Auch bei auf Debian basierenden Systemen ist es nicht sehr schwer:

    apt-get install scons
    

    Wer SCons aus den Quellen übersetzen möchte, lädt sich den Tarball (oder die Zip-Datei) herunter, entpackt sie und wechselt ins scons Verzeichnis. Dort einfach

    python setup.py install
    

    eingeben und SCons wird in die üblichen Verzeichnisse ( /usr/local/ ) installiert. Dies erfordert allerdings Administratorrechte; besitzt man diese nicht, so kann man folgendermaßen ein anderes Verzeichnis angeben:

    python setup.py install --prefis=$HOME
    

    **
    3 Die Datei "SConstruct"
    **

    Diese Datei kann man in etwa mit einem Makefile oder einer Ant-XML-Datei vergleichen, denn in ihr stehen die Tasks und Targets, die SCons dann ausführt.
    Um die Aufgaben zu beschreiben, wird Python verwendet, d.h., dass diese Datei eigentlich ein Python-Skript ist. Im Unterschied zu Python-Skripten wird diese Datei aber nicht unbedingt geordnet abgearbeitet (siehe unten), sondern so, wie es die Erfüllung der Targets erfordert.
    Die "SConstruct" sollte man an der Wurzel im Projektverzeichnis abgelegen.

    **
    4 Erstellen von Programmen
    **

    Um zu zeigen, wie man mit SCons Programme erstellen kann, werden wir das allseits beliebte "Hallo Welt"-Beispiel verwenden:

    //hello_world.cpp
    #include <iostream>
    
    int main(int argc, char **argv) {
      std::cout<<"Hello World\n";
      return EXIT_SUCCESS;
    };
    

    Sehen wir uns die dazugehörige SConstruct-Datei an:

    #SConstruct
    #Kommentare beginnen in Python mit einem #-Zeichen
    Program("hello_world.cpp")
    

    Diese Anweisung sagt SCons zwei Dinge: Wir wollen (1) ein Programm erstellen, und zwar (2) aus der Datei "hello_world.cpp". Um den Build-Prozess zu starten, wechselt man in das Verzeichnis, in dem die Datei "SConstruct" liegt und tippt einfach

    scons
    

    in die Shell ein.
    Auf meinem Linux-System erscheint folgende Ausgabe:

    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    

    Nun wurden eine hello_world.o und eine Binary namens hello_world erstellt. Auf Windows-Systemen sollten die Dateien hello_world.obj und hello_world.exe heißen.
    Es ist im Übrigen möglich, in einer SConstruct mehrere Targets zu definieren, also z.B. zwei oder drei Programme zu erstellen (wobei die Reihenfolge, wie SCons die Builds durchführt, nicht der Datei SConstruct folgen muss, d.h. es könnte sein, dass KillerApplication vor hello_world erstellt wird):

    Program("hello_world.cpp") #Erstes Target
    Program("KillerApplication.cpp") #Zweites Target
    

    Möchte man nur Objektdateien erstellen, muss man das Target in der SConstruct leicht verändern:

    #SConstruct
    #Einfache Anführungszeichen gehen auch:
    Object('hello_world.cpp')
    

    Um den Effekt zu sehen, müssen wir zuerst die Ergebnisse des vorangegangenen Builds löschen. Dies geschieht mit einem Aufruf von scons und dem -c (oder --clean) Parameter:

    scons -c
    

    Führt man scons nun erneut aus, so wird nur eine hello_world.o bzw. hello_world.obj erstellt.

    Die Ausgaben, die SCons um die eigentlichen Befehle erzeugt, können leicht mit dem -Q Parameter unterdrückt werden, um nur die wirklich wichtigen Informationen zu sehen:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o hello_world.o hello_world.cpp
    

    Bisher hat SCons immer die Namen für die erzeugten Programme vergeben; das können wir aber auch selbst:

    #SConstruct
    Program("Hello", "hello_world.cpp")
    

    Aus hello_world.cpp wird nun eine Binary namens Hello erstellt:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -o Hello hello_world.o
    gpc@darkstar:~/tmp$ ls -l
    total 28
    -rwxr-xr-x  1 gpc users 12755 2005-12-18 16:38 Hello*
    -rw-r--r--  1 gpc users    36 2005-12-18 16:37 SConstruct
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:31 hello_world.o
    

    Man sieht, es wurde eine Hello erstellt.

    4.1 Keyword Arguments
    *

    Wenn man die Quelldateien zuerst auflisten möchte und dann erst den Namen des Targets, so muss man mit so genannten "Keyword Arguments" arbeiten:

    #Standardreihenfolge
    Program(target = Hello, source = "hello_world.cpp")
    
    #Umgekehrte Reihenfolge
    Program(source = "hello_world.cpp", target = Hello)
    

    4.2 Mehrere Quelldateien
    *

    Größere Programme bestehen selten aus einer einzelnen Quelldatei (sollten sie jedenfalls nicht). SCons gibt uns selbstverständlich die Möglichkeit, mehrere Quelldateien zu kompilieren:

    #SConstruct
    #Nehmen wir an, es gibt noch eine goodbye.cpp
    Program("Hello", ["hello_world.cpp", "goodbye.cpp"])
    

    Ein Aufruf zeigt uns Folgendes:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o goodbye.o goodbye.cpp
    g++ -o Hello hello_world.o goodbye.o
    gpc@darkstar:~/tmp$ ls -l
    total 32
    -rwxr-xr-x  1 gpc users 12799 2005-12-18 16:51 Hello*
    -rw-r--r--  1 gpc users    53 2005-12-18 16:51 SConstruct
    -rw-r--r--  1 gpc users     0 2005-12-18 16:51 goodbye.cpp
    -rw-r--r--  1 gpc users   625 2005-12-18 16:51 goodbye.o
    -rw-r--r--  1 gpc users   108 2005-12-18 16:31 hello_world.cpp
    -rw-r--r--  1 gpc users  2264 2005-12-18 16:40 hello_world.o
    

    Hier sieht man, dass die Datei goodbye.cpp erfolgreich kompiliert und in Hello gelinkt wurde.

    Dieses Konstrukt mit den eckigen Klammern in der SConstruct ist eine Python-Liste, welche die einzelnen Quelldateien beinhaltet. Es wäre auch möglich, nur einen Eintrag in die Liste zu setzen. Dies hätte den gleichen Effekt wie vorher, als wir nur einen String mit dem Namen übergeben haben:

    #Beide Statements haben den gleichen Effekt
    Program("Main", ["main.cpp"])
    #Program("Main", "main.cpp")
    

    Da das Schreiben und Lesen einer längeren Liste nicht sehr viel Spaß macht, kann man alternativ auch die Python-Split-Funktion aus dem String-Modul verwenden:

    #SConstruct
    Program("Calculator", Split("main.cpp calculator.cpp parser.cpp"))
    

    Auch möglich:

    #SConstruct
    fileList = Split("main.cpp calculator.cpp parser.cpp")
    Program("Calculator", fileList)
    

    Über mehrere Zeilen:

    #SConstruct
    fileList = Split("""main.cpp 
    		    calculator.cpp 
    		    parser.cpp""")
    Program("Calculator", fileList)
    

    Im letzten Beispiel wurde die "triple-quote"-Syntax verwendet, die es erlaubt, einen String über mehrere Zeilen hinweg zu definieren. Auch hier können es entweder drei doppelte oder drei einzelne Anführungszeichen sein.

    **
    5 Erstellen von Bibliotheken
    **

    Abgesehen von Programmen und Objektdateien kann SCons auch mit Leichtigkeit Bibliotheken erstellen, sowohl statische als auch dynamische.

    Erstellen wir eine kleine Bibliothek, die in je einer Quelldatei eine Grundrechenart bereitstellt (die Deklaration findet sich in den dazugehörigen *.hpp Dateien):

    #SConstruct
    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Library erstellt eine statische Bibltiothek
    Library(target="math", source=fileList)
    

    Um nun die "Bibliothek" ~Wieso Anführungszeichen???~ zu erstellen, rufen wir scons auf, was folgende Ausgabe produziert:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -c -o add.o add.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o mul.o div.o
    ranlib libmath.a
    ar: creating libmath.a
    

    Unsere Bibliothek wurde erfolgreich erstellt.

    Wer sich gerne exakt ausdrückt, der kann anstatt Library auch StaticLibrary schreiben, wenn er eine statische Bibliothek erstellen möchte. Ein Unterschied existiert nicht.

    Das Erstellen einer dynamischen Bibliothek (*.so auf POSIX-Systemen, *.dll auf Windows-Systemen) ist genauso einfach:

    fileList = Split("add.cpp sub.cpp mul.cpp div.cpp")
    
    #Nun erstellen wir eine dynamische Bibliothek
    SharedLibrary(target="math", source=fileList)
    

    Führen wir nun SCons aus:

    gpc@darkstar:~/tmp$ scons -Q
    g++ -fPIC -c -o add.os add.cpp
    g++ -fPIC -c -o div.os div.cpp
    g++ -fPIC -c -o sub.os sub.cpp
    g++ -fPIC -c -o mul.os mul.cpp
    g++ -shared -o libmath.so add.os sub.os mul.os div.os
    

    Wie wir sehen, hat SCons eine dynamische Bibliothek erstellt, und das mit so wenig Aufwand.

    **
    6 Einbinden von Bibliotheken
    **

    Natürlich wäre die beste Bibliothek nutzlos, wenn man sie nicht in seinen Programmen verwenden könnte. Damit dies möglich ist, muss man die Bibliothek natürlich in die Executable hineinlinken.

    Die main.cpp sieht so aus:

    #include <iostream>
    #include "add.hpp"
    
    int main(int argc, char **argv) {
      int result = add(2,7);
      std::cout<<result<<'\n';
      return EXIT_SUCCESS;
    };
    

    Die SConstruct so:

    #SConstruct
    Library("math", ["add.cpp", "sub.cpp", "div.cpp", "mul.cpp"])
    Program("main.cpp", LIBS=["math"], LIBPATH=".")
    

    Bemerkenswert ist, dass wir den Namen der Bibliothek einfach so angeben können, ohne uns über systemspezifische Präfixe Gedanken machen zu müssen.

    Wenn wir scons jetzt aufrufen, erhalten wir folgende Ausgabe:

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o add.o add.cpp
    g++ -c -o sub.o sub.cpp
    g++ -c -o div.o div.cpp
    g++ -c -o mul.o mul.cpp
    ar r libmath.a add.o sub.o div.o mul.o
    ranlib libmath.a
    ar: creating libmath.a
    g++ -c -o main.o main.cpp
    g++ -o main main.o -L. -lmath
    scons: done building targets.
    

    Über das oben verwendete LIBPATH-Argument sucht SCons selbstständig nach Bibliotheken in den angegebenen Verzeichnissen:

    #SConstruct
    Program("Test", LIBS = "someLib", LIBPATH = ['/usr/lib', '/usr/local/lib'])
    

    SCons sucht nun in den beiden Verzeichnissen, ob es eine "someLib"-Bibliothek findet. Kann keine Bibliothek gefunden werden, meldet sich SCons mit einer Fehlermeldung.

    **
    7 Abhängigkeiten erkennen
    **

    Kommen wir zu einem interessanteren Feature: der Erkennung von Abhängigkeiten. Wenn sich an unserem Programm etwas geändert hat und wir es neu kompilieren, dann wollen wir, dass nur das nötigste neu übersetzt wird und Teile, die sich nicht geändert haben, unangetastet bleiben.

    gpc@darkstar:~/tmp$ scons
    scons: Reading SConscript files ...
    scons: done reading SConscript files.
    scons: Building targets ...
    g++ -c -o hello_world.o hello_world.cpp
    g++ -o hello_world hello_world.o
    scons: done building targets.
    
    gpc@darkstar:~/tmp$ scons -Q
    scons: `.' is up to date.
    

    SCons hat erkannt, dass seit dem letzten Build keine Änderungen stattgefunden haben***,*** und hat gar nichts gemacht. Soweit so gut.
    Normalerweise erkennt SCons eine Änderung am Inhalt der Datei, nicht am Datum des letzten Zugriffs (wie make es macht).

    Will man das Verhalten von make, muss man die SourceSignatures-Funktion benutzen:

    #SConstruct
    Program("hello_world.cpp")
    
    #Standardverhalten:
    #SourceSignatures("MD5")
    
    #make-Verhalten:
    SourceSignatures("timestamp")
    

    Sofern ein Target von einem anderen abhängt (z.B. Programm von Bibliothek), kann man auch hier festlegen, anhand welcher Kriterien SCons über einen Rebuild entscheidet. Dies geschieht mittels der TargetSignatures-Funktion.
    Das Standardverhalten ist, dass SCons anhand einer Target-Signatur, die aus den Signaturen der Quelldateien besteht, entscheidet, ob ein Rebuild notwendig ist:

    Program("hello_world.cpp")
    TargetSignatures("build")
    

    Wenn sich eine Quelldatei aber so verändert hat, dass der Inhalt des Targets der gleiche bleibt, dann ist kein kompletter Rebuild notwendig. Dieses Verhalten kann man so erreichen:

    Program("hello_world.cpp")
    TargetSignatures("content")
    

    Nun ist es gängige Praxis, die Schnittstelle einer Klasse in eine Header-Datei zu schreiben und die Implementation in eine Source-Datei auszulagern. Sehen wir uns folgendes kleines Programm an:

    //foo.hpp
    #ifndef FOO_HPP
    #define FOO_HPP
    
    struct Foo {
      void bar() const;
    };
    
    #endif
    
    //foo.cpp
    #include "foo.hpp"
    #include <iostream>
    
    void Foo::bar() const {
      std::cout<<"Blub blub\n";
    };
    
    //hello_world.cpp
    #include "foo.hpp"
    
    int main() {
      Foo f;
      f.bar();
      return 0;
    };
    

    Wenn sich an foo.hpp etwas ändert, will ich, dass SCons das erkennt und einen Rebuild durchführt, und das sage ich SCons mit der CPPPATH-Variable:

    #SConstruct
    Program(['hello_world.cpp','foo.cpp'], CPPPATH = '.')
    

    Nun sieht SCons im aktuellen (".") Verzeichnis nach, um herauszufinden ob ein Rebuild notwendig ist.
    Analog zu der LIBPATH-Variable, kann die CPPPATH-Variable eine Liste sein:

    #SConstruct
    Program('hello_world.cpp', CPPPATH = ['.', '/home/gpc/tmp/include'])
    

    **
    8 Auf Abhängigkeiten testen
    **

    Viele Programme benutzen externe Bibliotheken, um fehlende Funktionalität einer Standardbibliothek auszugleichen. Wenn der Benutzer unser Programm benutzen will, dann braucht er ebenfalls diese Bibliothek, um das Programm erfolgreich kompilieren zu können. Hat er die Bibliothek nicht installiert, wird der Kompiliervorgang abbrechen, da der Compiler die Abhängigkeiten nicht auflösen kann. Nicht sehr schön. Eine elegantere Lösung wäre doch, vorher abzufragen, ob Bibliothek Foo existiert, und bei Nichtvorhandensein eine entsprechende Fehlermeldung auszugeben (bei den autotools wird dies von autoconf erledigt). Um dies zu bewerkstelligen, müssen wir zuerst einen kleinen Abstecher zum Thema Construction Environments machen:

    8.1 Construction Environments
    *

    Eine so genannte Construction Environment gibt uns die Möglichkeit zu bestimmen, wie unsere Programme erstellt werden, welcher Compiler genutzt wird oder welche Flags gesetzt werden.

    Mit folgendem Python-Code kann man eine Construction Environment erstellen:

    #SConstruct
    env = Environment()
    

    Hierbei wird die Construction Environment 'env' mit den auf dem System installierten Tools initialisiert, also wird z.B. bei installiertem g++ der CC-Wert auf g++ gesetzt. Ebenso sieht es mit dem Linker und den anderen für den Build-Prozess benötigten Daten aus.
    Aber es ist eben immer wieder das gleiche mit Default-Werten: Meistens wollen wir spezielle Parameter übergeben. Nun, das ist ein Leichtes:

    #SConstruct
    env = Environment(CC = 'gcc', CCFLAGS = '-O2 -g')
    
    #Ersetzen von gcc durch g++, ist ja ein cpp file
    env.Replace(CC = 'g++')
    
    #Anhaengen von -W Option
    #Wuerde CCFLAGS noch nicht existieren, wuerde es angelegt werden
    env.Append(CCFLAGS = ' -W')
    
    #Alternativ kann man Prepend zum Voranstellen benutzen:
    env.Prepend(CCFLAGS = ' -Weffc++ ')
    
    #Achtung: Neuer Aufruf:
    env.Program('main.cpp')
    

    An der Ausgabe erkennen wir, dass scons unsere Angaben übernommen hat:

    bash-3.00$ scons -Q
    g++ -Weffc++ -O2 -g -W -c -o main.o main.cpp
    g++ -o main main.o
    

    Eine Construction Environment verfügt, vereinfacht gesagt, über ein so genanntes Dictionary (assoziatives Array), welches u.a. die CC-Variable mit dem zugeordneten Wert 'g++' beinhaltet. Somit kann man leicht nachschauen, welchen Wert die einzelnen Variablen haben. Geben wir doch einfach mal alle in der Shell aus:

    #SConstruct
    env = Environment()
    
    #Einzelnes Element auswaehlen und ausgeben
    print "Used compiler: ", env['CC']
    
    dict = env.Dictionary() #Dictionary holen
    for i,j in dict.iteritems(): #Über alle Elemente iterieren
      print i,j		     #...und Key-Value-Paar ausgeben
    

    Mit mehreren Construction Environments können wir auch mehrere Verhaltensmuster nachbilden, um z.B. einen Debug- und einen Release-Vorgang zu erstellen:

    #SConstruct
    release = Environment(CCFLAGS = '-O2 -Os')
    debug = Environment(CCFLAGS = '-g')
    
    release.Program('foo', 'foo.cpp')
    debug.Program('bar', 'bar.cpp')
    

    8.2 Auf Bibliotheken/Header/Funktionen/typedefs testen
    *

    Was wollten wir eigentlich in diesem Abschnitt erreichen? Richtig: Prüfen auf Abhängigkeiten. Jetzt sind wir in der Lage, dies zu tun.
    Zuerst erstellen wir wie gehabt unsere Construction Environment. Dann verknüpfen wir einen Configure Context mit der Construction Environment, um die Tests durchzuführen, und beenden den Vorgang schließlich mit der Finish()-Methode:

    #SConstruct
    env = Environment()
    conf = Configure(env)
    
    #Auf Bibliothek pruefen
    if not conf.CheckLib('gdk'):
      print "Couldn't find gdk library. Exiting."
      Exit(1)
    
    #Auf C-Header pruefen
    if not conf.CheckCHeader('assert.h'):
      print "Couldn't find assert.h. Exiting."
      Exit(1)
    
    #Bei Vorhandensein von Header Flag mitgeben
    if conf.CheckCHeader('myHeader.h'):
      conf.env.Append('-DHAS_MY_HEADER_H')
    
    #Auf CPP-Header pruefen
    if not conf.CheckCXXHeader('list'):
      print "Couldn't find list. Exiting."
      Exit(1)
    
    #Auf Funktion testen
    if not conf.CheckFunc('memset'):
      print "Couldn't find memset. Exiting"
      Exit(1)
    
    #Auf typedef testen
    if not conf.CheckType('time_t', '#include <time.h>\n'):
      print "Couldn't find typedef time_t. Using long instead."
      conf.env.Append(CCFLAGS = '-Dtime_t=long')
    
    env.Program('main.cpp')
    
    env = conf.Finish()
    

    Es ist auch ohne weiteres möglich, benutzerdefinierte Tests zu bauen, aber dies würde den Rahmen sprengen, deshalb muss ich ein weiteres Mal auf das Tutorial verweisen.

    **
    9 Zum Schluss
    **

    Das war jetzt nur eine kleine Vorstellung von dem, was SCons kann. Alles darzustellen war aber auch nicht mein Anspruch, deshalb sei an dieser Stelle noch mal auf das offizielle SCons-Tutorial verwiesen, welches sehr viele Informationen enthält und auf (fast) alle Fragen eine Antwort bietet.

    **
    10 What comes next...
    **

    Im nächsten Artikel wird uns Artchi dann bjam, das Build-System von boost, erklären.



  • die orthgraphie ist eigentlich sehr gut, so gut wie fehlerfrei. eines aber sei gesagt:

    achte, wann der nebensatz zu ende ist. manchmal lässt du das komma weg und dann bekommt das alles (manchmal, nicht immer) n ganz anderen sinn.

    "er wollte, dass sie ging und vertrieb sie"
    ➡ "Er wollte, dass sie ging, und vertrieb sie."

    Mr. B



  • Hallo,

    first of all: Thanks.

    die orthgraphie ist eigentlich sehr gut, so gut wie fehlerfrei. eines aber sei gesagt:

    Die was??? ➡ Google anwerf ➡ Ah, jetzt ist's klar. 🤡 Merci beaucoup.

    achte, wann der nebensatz zu ende ist. manchmal lässt du das komma weg und dann bekommt das alles (manchmal, nicht immer) n ganz anderen sinn.

    Okay, werd schauen dass ich das in den Griff kriege.

    Um nun die "Bibliothek" ~Wieso Anführungszeichen???~ zu erstellen, rufen wir scons auf, was folgende Ausgabe produziert:

    Weil sie nur die vier Grundrechenarten beherrscht... 😃

    MfG

    GPC


Anmelden zum Antworten