twm's homepage logo
Von einem, der auszog die Heimat schätzen zu lernen ...
Delphi und Make
Deutsch English
Google
Search dummzeuch.de
Search WWW

Warnung

Ich bin alles andere als ein Experte in Sachen Makefiles und beschreibe hier lediglich, was ich verwendet habe bzw. glaube verstanden zu haben. Das folgende kann voellig falsch sein. Ich uebernehme keine Garantie irgendwelcher Art und hafte nicht fuer Schaeden, die durch Verwendung der folgenden Informationen entstehen koennen.

Make? Was ist das?

Wer sich mal den Quelltext der automatisch erzeugten *.bpg-Dateien (Borland Project Group, erzeugt ab mindestens Delphi 5) angesehen hat, der wird sich gefragt haben, wozu die eigentlich gut sind. Eine solche Datei sieht typischerweise so aus:

                      
#---------------------------------------------------------
VERSION = BWS.01
#---------------------------------------------------------
!ifndef ROOT
ROOT = $(MAKEDIR)\..
!endif
#---------------------------------------------------------
MAKE = $(ROOT)\bin\make.exe -$(MAKEFLAGS) -f$**
DCC = $(ROOT)\bin\dcc32.exe $**
BRCC = $(ROOT)\bin\brcc32.exe $**
#---------------------------------------------------------
PROJECTS = Project1.exe Project2.exe
#---------------------------------------------------------
default: $(PROJECTS)
#---------------------------------------------------------

Project1.exe: Project1.dpr
  $(DCC)

Project2.exe: Project2.dpr
  $(DCC)
                    

Leute mit C oder Unix / Linux Erfahrung werden dieses Format evtl. wiedererkennen. So sehen typische Makefiles aus. Fuer alle anderen ist wohl eine Erklaerung angebracht: Make ist ein Tool, mit dem haeufig bei C Programmen die einzelnen Schritte der Programmerstellung automatisiert werden, also in der Regel mindestens

  1. Aufruf des Compilers fuer alle .c Dateien um Objekt-Dateien (.o) zu erzeugen.
  2. Aufruf des Linkers fuer alle in Schritt 1 erzeugten .o Dateien, um ein Executable zu erzeugen.

Ein Makefile beinhaltet eine Beschreibung dessen, was zu tun ist. Make kann wesentlich mehr als das oben aufgezaehlte, z.B. beliebige Programme aufrufen, nicht nur einen C-Compiler und Linker. Auch Delphi bringt ein Make-Programm mit, es befindet sich dort, wo alle Executables stehen, in einer Standardinstallation unter 'c:\programme\borland\delphi5\bin'.

Warum Make?

Warum sollte man ueberhaupt Make verwenden, wo doch Delphi so eine schoene IDE hat?

Die IDE ist wunderbar zum Entwickeln und interaktiven Compilieren von Programmen. Sie eignet sich weniger gut, um ein bestehendes Projekt automatisch zu erzeugen. Dazu werden haeufig Batchdateien verwendet, die jedoch um einiges weniger flexibel sind als ein Makefile.

Ich selbst habe fuer einige Zeit die erweiterte Batchsprache von 4NT zu diesem Zweck eingesetzt, da mir die Moeglichkeiten des Standard Commandline Interpreters unter Windows zu begrenzt waren. Make habe ich erst letzte Woche 'entdeckt' und stellte fest, dass sich damit einiges wesentlich einfacher automatisieren lies als mit 4NT.

Der Aufbau einer .bpg-Datei

Nochmal das Beispiel von oben:

                    
#---------------------------------------------------------
VERSION = BWS.01
#---------------------------------------------------------
!ifndef ROOT
ROOT = $(MAKEDIR)\..
!endif
#---------------------------------------------------------
MAKE = $(ROOT)\bin\make.exe -$(MAKEFLAGS) -f$**
DCC = $(ROOT)\bin\dcc32.exe $**
BRCC = $(ROOT)\bin\brcc32.exe $**
#---------------------------------------------------------
PROJECTS = Project1.exe Project2.exe
#---------------------------------------------------------
default: $(PROJECTS)
#---------------------------------------------------------

Project1.exe: Project1.dpr
  $(DCC)

Project2.exe: Project2.dpr
  $(DCC)
                  

Was bedeutet das nun alles?

Es gibt in diesem Beispiel folgende Hauptelemente:

  1. Kommentare, dies sind die Zeilen, die mit einem # beginnen.
  2. Makrozuweisungen, das sind alle Zeilen, die mit einem (meist gross geschriebenen) Makronamen, gefolgt von einem Gleichheitszeichen beginnen. Dem Makro wird als Wert das zugewiesen, was rechts vom Gleichheitszeichen steht.
  3. Make-Anweisungen, das sind die Zeilen, die mit einem ! beginnen. Hier wird die Abarbeitung des Makefiles gesteuert. Im obigen Beispiel bedeutet '!ifndef ROOT', dass die Zeile bis zum folgenden '!endif' nur dann ausgefuehrt werden sollen, wenn das Makro ROOT keinen Wert hat.
  4. Make Targets (Ziele), das sind die Zeilen, die mit einem Wort (meist einem Dateinamen) gefolgt von : beginnen. Rechts vom Doppelpunkt stehen alle Dateien oder andere Targets, die das jeweilige Target benoetigt, um erzeugt zu werden. Die Zeilen danach, die eingerueckt sind, sind Anweisungen an Make, was es zu tun hat. (Hinweis: Unix-Makefiles benutzen zur Einrueckung normalerweise ein TAB (Ascii 9) Zeichen, Borlands Make scheint jedoch auch Spaces zu akzeptieren.)

Kommentare

Zu Kommentaren muss ich wohl nicht viel sagen. ;-)

Makrozuweisungen

Auf der rechten Seite des Gleichheitszeichens kann auch ein Ausdruck der Form $(MAKRONAME) vorkommen. Das bedeutet, dass der Inhalt des Makros MAKRONAME an dieser Stelle eingesetzt wird. Die Zeile

                  
ROOT = $(MAKEDIR)\..
                

bedeutet, dass dem Makro ROOT der Wert des Makros MAKEDIR gefolgt von '\..' zugewiesen wird. MAKEDIR ist ein vordefiniertes Makro, welches als Wert das Verzeichnis enthaelt, in dem sich das Make Executable befindet. Hat also z.b. MAKEDIR den Wert 'c:\programme\borland\delphi5\bin', so erhaelt ROOT den Wert 'c:\programme\borland\delphi5\bin\..'. Die folgende Anweisung

                
DCC = $(ROOT)\bin\dcc32.exe $**
              

weist dann dem Makro DCC den Wert des Makros ROOT gefolgt von 'bin\dcc32.exe $**' zu. Damit ergibt sich fuer DCC der Wert 'c:\programme\borland\delphi5\bin\..\bin\dcc32.exe $**'.

Hinweis: Es ist nicht moeglich, ein Makro sich selbst zuzuweisen, d.h. folgendes

              
ROOT = $(ROOT)\bin\

            

fuehrt zu einem Fehler, weil Make diese Anweisung rekursiv unendlich oft auszufuehren versucht.

Make Anweisungen

Ausser dem im Beispiel angegebenen !ifndef/!endif gibt es noch weitere:

  • !elif ist eine Kombination aus !else und !if, welches ein zusaetzliches !endif einspart.
  • !else brauche ich wohl nicht erklaeren ;-)
  • !error, gefolgt von einer Fehlermeldung gibt diese Meldung aus und beendet Make
  • !if erlaubt zusaetzlich zu !ifdef und !ifndef andere Bedingungen
  • !include, gefolgt von einem Dateinamen, fuegt eine andere Datei an der Stelle ein, wo diese Anweisung steht.
  • !message, gefolgt von einer Meldung, gibt diese auf den Bildschirm aus.
  • !undef, gefolgt von einem Makronamen loescht dieses Makro, danach ergibt ein !ifdef auf dieses Makro false.

Make Targets

(Target = Ziel, deshalb 'das Target', oder?)

Obiges Beispiel enthaelt drei Make-Targets, naemlich

  • default
  • Project1.exe und
  • Project2.exe

Alle diese Targets sind explizite Targets, d.h. sie erzeugen einen kompletten Dateinamen, nicht nur eine Extension.

Das Target 'default' enthaelt keine Anweisungen sondern lediglich ein Liste von Vorbedingungen:

            
default: $(PROJECTS)
          

Dies bedeutet, dass alle in dem Makro PROJECTS angegebenen Dateien (bzw. Targets) erzeugt werden muessen, bevor das Target default erzeugt werden kann.

Das Target 'Project1.exe' hat die Vorbedingung 'Project1.dpr', d.h. um es zu erzeugen, muss die Datei 'Project1.dpr' existieren und neuer sein als 'Project1.exe':

          
Project1.exe: Project1.dpr
  $(DCC)
        

Wenn diese Bedingung erfuellt ist, werden die danach augefuehrten Befehle ausgefuehrt. In diesem Fall wird der Wert des Makros DCC als Befehl betrachtet. Wie schon weiter oben festgestellt, hat es den Wert 'c:\programme\borland\delphi5\bin\..\bin\dcc32.exe $**'. Der erste Teil davon duerfte klar sein, es handelt sich um den Aufruf des Delphi Commandline Compilers, der sich im Unterverzeichnis 'bin' der Delphi 5 Installation befindet. Als Parameter wird $** uebergeben, ein spezielles vordefiniertes Makro, welches Make durch die Liste der Dateien ersetzt, die das Target als Voraussetzungen hatte, in diesem Fall also 'Project1.dpr'. Der komplette Befehl sieht also wie folgt aus:

        
c:\programme\borland\delphi5\bin\..\bin\dcc32.exe Project1.dpr
      

Dies ruft den Compiler auf, der das Projekt neu compiliert.

Make versucht immer das erste explizite Target automatisch zu erzeugen, wenn man ihm kein Target vorgibt. In obigem Beispiel ist das das Target 'default', aber der Name ist beliebig.

Aufruf von Make mit einer .bpg Datei

Ruft Make in einem Verzeichnis auf, in dem sich eine Delphi Projekt Group befindet, so wird man enttaeuscht, es passiert nichts:

      
Fatal: Unable to open makefile
    

Ein etwas erfahrenerer Windows Anwender (oder vielleicht eher DOS Anwender) wird vielleicht versuchen, die .bpg Datei als Parameter zu uebergeben, aber auch das fuehrt nur zu der gleichen Fehlermeldung.

Ruft man Make mit der Option -h oder -? auf, so erhaelt man eine kurze Zusammenfassung der Optionen. Daraus geht hervor, dass man die .bpg-Datei mit der Option -f angeben muss:

    
make -f ProjectGroup.bpg
  

fuehrt endlich zum Erfolg. Der Compiler wird fuer beide Projekte aufgerufen und diese erzeugt.

Wer es einfacher mag, der kann eine Datei 'Makefile' mit folgendem Inhalt im Projektverzichnis erstellen:

  
!include ProjectGroup.bpg

Make sucht naemlich, wenn es keine -f Option uebergeben bekommt nach einer Datei diesen Namens. Die !include Anweisung bewirkt dann, dass die eigentliche .bpg Datei ausgewertet wird.

Fallen und Probleme

DCC32 und .cfg-Dateien

Der Delphi Commandline Compiler (dcc32) hat ein Feature, welches evtl. aergerliche Auswirkungen haben kann. Er liest seine Konfiguration aus der Datei '<projektname>.cfg'. Diese Datei wird von der IDE automatisch erzeugt und enthaelt die Einstellungen des Projekts, wie sie auch in der .dof-Datei stehen, nur eben in dem Format, wie es dcc32 versteht. Das hoert sich jetzt an, als sei es eine gute Sache und meist ist es das auch, aber in der Regel benutzt man Makefiles um Executables mit definierten Einstellungen zu erzeugen. Wenn nun aber die .cfg-Datei jedesmal geaendert wird, wenn man eine Einstellung in der IDE aendert, so kann man sich leicht auch wichtige Einstellungen, z.B. die fuer Debug-Informationen ueberschreiben, welche man in der Regel in automatisch erzeugten Executables nicht haben will.

Abhilfe schafft hier, die benoetigten Einstellungen im Makefile zu definieren und als Parameter and dcc32 zu uebergeben und die .cfg-Datei zu loeschen:

  
EXEOUT = q:\MyProject
UNITOUT = $(EXEOUT)\dcu
DCCOPT = -D- -I-

DCC = $(ROOT)\bin\dcc32.exe -E"$(OUTPUT)" -N"$(UNITOUTPUT)" $DCCOPT $**

Project1.exe: Project1.dpr
	del Project1.cfg
        $(DCC)

Wie man sieht, kann man in einem Makefile auch Shell-Befehle ausfuehren lassen, in diesem Fall 'del'.

Etwas komplizierter wird es, wenn man den 'del' Befehl nicht fuer jedes einzelne Projekt angben will. In diesem Fall kann man ein sogenanntes implizites Target verwenden, welches Make angibt, wie man aus einer .dpr Datei eine .exe Datei erzeugt:

  
Project1.exe: Project1.dpr

.dpr.exe:
	del $(<:.dpr=.cfg)
        $(DCC)

Die erste Zeile ist wieder die Angabe, dass 'Project1.exe' aus 'Project1.dpr' erzeugt wird (eigentlich ist sie in diesem Fall nicht notwendig, aber die Delphi IDE weigert sich, eine .bpg-Datei zu laden, wenn nicht fuer alle Projekte explizite Targets angegeben sind). Es gibt keine Regeln, wie dieses Target erzeugt wird. Deshalb sucht Make nach einem impliziten Target, welches beschreibt, wie man aus einer .dpr Datei eine .exe Datei erzeugt.

Dieses zweite Target sieht ziemlich kompliziert aus, weil es ein weiteres Feature von Make verwendet: String-Ersetzung in Makros.

Zunaechst ruft es den 'del' Befehl auf und uebergibt ihm das Makro $< als Parameter. Da wir aber nicht die .dpr Datei, sondern die .cfg Datei loeschen wollen, muss die Extension geaendert werden. Dies geschiet durch das Anhaengen von ':.dpr=.cfg' an das Makro. Zusaetzlich muss das Makro in Klammern gesetzt werden, damit Make erkennt, dass der gesamte Ausdruck und nicht nur das erste Zeichen zum Makro gehoert.

Die Zeile bedeutet also: Nimm eine .dpr Datei, ersetze alle Vorkommen von '.dpr' durch '.cfg' und uebergib das als Parameter fuer den 'del' Befehl.

Die zweite Anweisung ist dann wieder einfach der Aufruf des Commandline-Compilers.

Achtung: Wenn man in der IDE ein neues Projekt zu einer Project Group hinzufuegt, so erzeugt sie es nicht nur einen weiteren Eintrag fuer das Makro 'PROJECTS' sondern fuegt ein neues Target fuer dieses Projekt ein, in dem ein Aufruf fuer den Compiler steht. Will man obiges implizites Target verwenden, so muss man diesen Aufruf manuell aus der .dpg Datei loeschen.

Resource Dateien (.res)

Die IDE erzeugt zu jedem Projekt automatisch eine .res-Datei, die das Icon des Programms und die Versionsinformationen enthaelt. Wer wie ich ein Sourcecode-Verwaltungsprogramm verwendet, der mag es wahrscheinlich ebenfalls nicht, dass sich dort drin Binaerdateien ansammeln. Ich fuer mein Teil checke .res-Dateien nicht ein sondern lasse sie jedesmal von der IDE neu erzeugen. Die Versionsinformationen bleiben dabei erhalten, denn diese nimmt die IDE aus der .dof-Datei. Das Programmicon wird jedoch nur in der .res-Datei gespeichert und geht damit verloren.

Das ist ziemlich aergerlich und deshalb habe ich mir folgende Loesung fuer das Problem ausgedacht:

Ich speichere das Programmicon als <projectname>.ico im gleichen Verzeichnis wie die .dpr Datei.

Nun rufe ich bei jedem Compile-Vorgang ein selbstgeschriebenes (Delphi-)Programm, welches eine .dof-Datei ausliest (das ist eine INI-Datei, man kann also TIniFile benutzen) und mir daraus eine .rc-Datei mit den Versionsinformationen generiert und auch gleich die .ico-Datei, falls sie existiert, an diese anhaengt:

  
// Versionsinformationen hier...

MAINICON ICON LOADONCALL MOVEABLE DISCARDABLE IMPURE <projectname>.ico

Aus der so erzeugten .rc-Datei kann man dann mit dem Borland Resource Compiler wieder eine .res-Datei erzeugen. Und damit man das nicht manuell machen muss, trägt man die Aufrufe einfach in das Makefile ein:

  
#---------------------------------------------------------
VERSION = BWS.01
#---------------------------------------------------------
!ifndef ROOT
ROOT = $(MAKEDIR)\..
!endif
#---------------------------------------------------------
MAKE = $(ROOT)\bin\make.exe -$(MAKEFLAGS) -f$**
DCC = $(ROOT)\bin\dcc32.exe $**
BRCC = $(ROOT)\bin\brcc32.exe $**
#---------------------------------------------------------
# Mein Utility:
DOF2RC = Dof2Rc.exe $**
#---------------------------------------------------------
PROJECTS = Project1.exe Project2.exe
# die Resource-Dateien, wieder mit String-Ersetzung
RESOURCES = $(PROJECTS:.exe=.res)
#---------------------------------------------------------
default: $(RESOURCES) $(PROJECTS)
#---------------------------------------------------------

# diese beiden Eintraege, damit die IDE nicht motzt
Project1.exe: Project1.dpr

Project2.exe: Project2.dpr

.dof.rc:
	&$(DOF2RC)

.rc.res:
	&$(BRCC)

Auf diese Weise erhaelt man mit einem Aufruf von Make fertige Executables inclusive Icons und Versionsinformationen.

Natuerlich kann es sein, dass das alles noch viel einfacher geht. Ich bin, wie schon oben gesagt, alles andere als ein Makefile-Experte.

Weiterfuehrende Informationen

Leider scheint Borland vergessen zu haben, die Dokumentation zu Make in die Delphi-Installation aufzunehmen. Unter folgender Adresse kann man sich jedoch die Hilfe zu den Commandline-Tools des Borland C++ Builders herunterladen, in der unter anderem auch Make beschrieben wird: ftp://ftp.borland.com/pub/bcppbuilder/techpubs/bcb5/b5tool.zip

(c) Copyright 2002 by Thomas Mueller, alle Rechte vorbehalten



This document was generated using AFT v5.096

letzte Änderung: 2012-10-14 twm
Post to del.icio.us Best Viewed With Open EyesValid XHTML 1.0!