Karawane

 

$EasyFileGenerator = &Perl($Imake)

Ein Vortrag vom Perl Workshop 2004

Karawane

 

Der EasyFileGenerator ist ein Datei Generator geschrieben in Perl, der nicht nur Makefiles in paar Augenblicken generieren kann.

Mit diesem Tool lassen sich alle Make-Träume erfüllen und Alpträume in Zukunft vermeiden! ;-)

Inhalt


$EasyFileGenerator = &Perl($Imake) - Ein Makefile Generator der einfachen Art

Autor

Ralf Peine <ralf.peine@jupiter-programs.de>

Aufgewachsen in Bochum (geb. 1965) siedelte ich nach dem Mathematik-Studium in Dortmund (Diplom 1991) nach Erlangen über und entwickelte bei der Siemens AG (KWU OI41) SW-Tools für die Kraftwerksleittechnik. 1994 schrieb ich meine ersten Perl-Scripts unter HP UX (Unix) u.a. für SQL-Datenbanken und das Configuration Management. Seit 1998 arbeite ich für die SW-Entwicklung der Cordless Products (ICM CP RD SD GES SDS) in Bocholt und entwickelte dort u.a. größere Tools mit Perl unter Microsoft Windows. 2001 nahm ich zum ersten Mal am "Deutschen Perl Workshop" teil und hielt 2003 meinen ersten Vortrag im Perl-Workshop über "Objektorientiertes Perl, XML und große Datenmengen".

Abstract

Wer sich schon mal mit dem Make- / Build-Management-Tool Imake beschäftigt (oder gequält) hat, kennt vermutlich auch die folgende, besonders hilfreiche Fehlermeldung vom cpp:

 Error in line 12347: ")" missing...

Da der cpp keine Variablen, sondern nur Macros (Funktionen) kennt, muss man dieses Manko bei Imake durch Verwendung von genügend Klammern ausgleichen:

 SRC (Comfort, (HP800, WIN32), CONCAT(DIR(util/calc), calc.c) ...

Dass oben eine Klammer fehlt, sieht man doch auf den ersten Blick! (Oder etwa doch nicht ?)

Der Klammerdschungel in den Regeln trieb mich fast zur Verzweiflung, als ich '97 unser CM (unter HP-UX und SCO-Unix) von RCS auf ClearCase umstellen und dazu alle Regeln anpassen musste.

Inzwischen arbeite ich seit 6 Jahren unter Microsoft Windows. Windows hat u.a. einen entscheidenden Unterschied zu Unix: Es gibt dort kein Imake! Das stellte mich vor vier Jahren vor die Wahl:

  1. Imake zu portieren oder
  2. selbst einen Generator (mit Perl) zu schreiben.

"Schweren Herzens" entschied ich mich für (2) und entwickelte einen eigenen Generator. Jetzt, mit dem 3. Design, ist mir mit dem "Easy File Generator" ein OO-Entwurf gelungen, den ich Euch in einem Vortrag vorstellen möchte, weil er einige Perl-Features (u.a. require und compile zur Laufzeit) optimal ausnutzt und so leicht nur in Perl realisierbar ist.

Der Easy File Generator erzeugt durch den Aufruf von Regeln in *.efg-Dateien beliebig viele weitere Dateien. Für die Anwender sind keine Programmierkenntnisse in Perl oder anderen Programmiersprachen erforderlich. Anführungszeichen, Escape-Sequenzen, Kommata habe ich in der Anwendung der Regeln nahezu komplett gestrichen, Klammern werden nur für Variablen z.B. "$(platform)", "$(suffix)" benötigt:

 &SRC   Comfort   HP800         ui/X11/display   display.c
 &OUT   Comfort   Win32|HP800   ui\$(platform)   viewer$(suffix)

Die Regeln werden direkt in Perl geschrieben, man kann alle (wirklich alle!) Perl-Statements verwenden und sogar - wenn man das gut findet und nicht anders kann - alles in einer *.efg-Datei beliebig miteinander kombinieren. Die Möglichkeiten von Perl zum Tracen und Debuggen erleichtern das Erstellen der Regeln enorm. Syntaxfehler in den *.efg Dateien werden mit Angabe der fehlerhaften Zeile/Zeichen gemeldet. Das macht das Generieren von Files wirklich "easy"! (Hoffentlich sehen die Anwender das genauso...)

Im Prinzip läuft der EasyFileGenerator auf jeder Plattform, auf der es Perl gibt, da er ausschliesslich plattformunabhängige Perl-Module verwendet. Testen konnte ich bisher nur Linux und Win32, aber ich erwarte für andere Plattformen keinen nennenswerten Portierungsaufwand.

Die Anwendungsmöglichkeiten des EasyFileGenerator sind vielfältig, er lässt sich z.B. auch zum Erstellen von Web-Seiten oder zum Katalogisieren von DVDs verwenden.

Einleitung

Ende 1999 erhielt ich den Auftrag, für die neu gegründete Bluetooth Abteilung bei Siemens in Bocholt eine Software Entwicklungsumgebung aufzubauen. Windows-NT als Betriebssystem und das Microsoft-Developer Studio als Developer Workbench waren als Eckpunkte vorgegeben.

Da uns das bisher verwendete SIEMAKE (eine Eigenentwicklung von Siemens Bocholt) wegen seiner fest einprogrammierten Regeln nicht flexibel genug war, suchte ich nach einem Makefilegenerator für Windows. Als Configuration Manager bei Siemens in Erlangen hatte ich bereits fünf Jahre Makefiles mit "Imake" generiert und wollte eine Implementierung für MS-Windows einsetzen - fand aber nur eine Java Variante, die nicht sehr vielversprechend erschien.

Das stellte mich vor die Wahl, entweder Imake zu portieren oder einen eigenen Makefilegenerator - natürlich in Perl - zu schreiben. Ich stellte also folgenden Anforderungskatalog auf:

Anforderungen für einen Makefile Generator

 1      Syntax
 1.1      Einfach
 2      Varianten
 2.1      Konfigurierbar
 2.2      Alle auf einmal
 2.3      Einzeln
 3      Zielsysteme
 3.1      Konfigurierbar
 3.2      Alle auf einmal
 3.3      Einzeln
 3.4      (Compiler)
 4      Regelsprache
 4.1      Mächtig
 4.2      Standard
 5      Fehlersuche
 5.1      Testausgaben
 5.2      Debugging
 6      Fehlermeldungen
 6.1      File, Zeile und Position
 7      Plattformen (Generator)
 7.1      Möglichst viele
 7.2      Möglichst plattformunabhängig,
          d.h. kein Portierungsaufwand bei Plattformwechsel des Makefile-Generators
 8      Überlagerung von allg. Rules durch projektspezifische

und implementierte dann - laut Lehrbuch - gemäß Wasserfallmodell

 Analyse -> Design -> Implementierung -> Test

den EasyFileGenerator, fertig!

Da das "real life" sich wie ein bockiger Esel nicht um Lehrbücher und Prozesse aus denselben schert, verlief die Entwicklung des EasyFileGenerators natürlich ganz anders. Wie genau, möchte ich im folgenden darstellen und dadurch auch die Gründe für meinen Ansatz zur Generierung von Makefiles erläutern. Von dem Anforderungskatalog waren mir damals (1999) z.B. erst ein Drittel der Punkte bekannt.

Wozu überhaupt Makefiles generieren?

Bevor der Easy File Generator vorgestellt wird, gebe ich hier noch eine kurze Einführung in Make, Make-Rules und SW-Build-Verfahren. Wer sich hier gut auskennt kann auch gleich im nächsten Kapitel "Einfache Syntax" weiterlesen.

Makefiles werden vom Configuration Management dazu verwendet, um ein Programm (-System) aufzubauen (build), vergleichbar mit einer vollautomatischen Produktionsstraße z.B. für Autos: Verschiedene Teile müssen auf unterschiedliche Weise hergestellt (geformt) und in der richtigen Reihenfolge montiert werden. Komplizierte Abfolgen, Vorratshaltungen usw. sind möglichst zu vermeiden weil schwer zu automatisieren und fehleranfällig.

Build ohne Make

Das allseits bekannte "Hello World" Programm (hello.c) läßt sich noch vom Compiler "zusammenschrauben":

 // Hello world
 include <stdio.h>
 void main()
 {
   print "Hello World!\n";
 }
 >cc hello.c

fertig ist das Programm. Perl benötigt ebenfalls kein Make, weil Perlscripts vor dem Start von Perl compiliert werden und in den Scripts selbst die Reihenfolge der Compilierung festgelegt ist. (Der Perl-Kernel die C-Libraries sind beim Aufruf von perl ja bereits "gemaked".)

Müssen in C mehrere Files compiliert werden, kann man Batches oder Shell Scripts einsetzen. Dann wird jedes File bei jedem neuen Build-Lauf erneut compiliert. Das führt zu großen Laufzeitverlusten, wenn man mehrere tausend Files compilieren muss, obwohl sich nur ein einziges geändert hat. (Microsoft hat das aber nicht davon abgehalten, für die WindowsME Entwicklungsumgebung statt Make Batches einzusetzen...)

Standard Make

Um die Laufzeit zu verkürzen, kann man das Tool "Make" und Makefiles verwenden. Make sorgt dafür, dass nur die Objects neu erzeugt (compiliert) werden, deren Sourcen seit dem letzten Build verändert wurden. Dazu werden von Make die Zeitstempel verglichen: Ist die Source Datei neuer als das dazugehörige Object, wird es neu compiliert. Damit das funktioniert, muss Make wissen, welches Object durch die Compilierung welcher Source File (s) erzeugt wird. Dazu werden im einfachsten Fall die folgenden Build Rules aufgestellt:

 OBJECTS = a.o b.o c.o d.o      # (usw.)
 .c.o :
        $(CC) $(CFLAGS) .c $<
 file.exe: $(OBJECTS)
        $(LD) -o file.exe $(LFLAGS) $(OBJECTS)
 ALL:: file.exe

mit

 make ALL

wird das Programm schliesslich vollautomatisch von Make generiert (build).

Abhängigkeiten von Header.Files (*.h)

Wenn ein C-File a.c ein Header-File a.h includiert, muss a.c auch neu übersetzt werden, wenn a.h geändert wurde. Damit make das auch weiß, müssen diese Abhängigkeiten in das Makefile eingetragen werden. Keine gute Idee ist das manuelle Pflegen der Abhängigkeitsliste, was fast zwangsläufig zu sehr schwer zu findenden SW-Fehlern führt. Besser ist es, die Abhängigkeiten von einem Tool erfassen zu lassen und in einer extra Datei zu speichern, die an das Makefile angehängt werden kann. Aber auch hier können Abhängigkeiten von den Tools übersehen werden.

Die einzige mir bekannte Lösung, in jedem Fall alle Abhängigkeiten zu ermitteln, bietet der Einsatz von ClearCase und omake (Windows) bzw. clearmake (UNIX). omake/clearmake arbeiten auf einem speziellen Filesystem von ClearCase, welches jedes gelesene File während des Compiles mitprotokolliert und sich dies für jedes durch make erzeugte Object im sogenannten Configuration Record speichert. Beim nächsten Makelauf können dann zusätzlich zu allen in den Regeln angegebenen Abhängigkeiten auch noch die im vorigen Lauf gespeicherten berücksichtigt werden, wodurch kein notwendiges Compile mehr ausgelassen wird. omake/clearmake erkennen sogar geänderte Make-Rules!

Make Erweiterungen

In größeren Projekten gibt es meist ein Files (hoffentlich nur wenige), die eine besondere Behandlung erfordern, spezielle Compilerschalter, Precompiles oder ähnliches. Mit Standard Make ist das nicht leicht zu handhaben, weil man dann für jede abweichende Datei eine eigene Makerule schreiben muss:

 a.o: a.c
        CC -o a.o $(CC_ARGS_a) a.c
 b.o: b.c
        CC -o b.o $(CC_ARGS_b) b.c
 c.o: c.c
        CC -o c.o $(CC_ARGS_c) c.c
 ...

Änderungen an den Makerules werden jetzt zu einer aufwendigen und fehleranfälligen Aufgabe, denn eine (generierte) Makezeile aus einem realen Projekt sieht z.B. so aus:

 ..\p_appl\cfgsrv\pc_prc.o66: ..\p_appl\cfgsrv\pc_prc.c
        @echo --- Target Compile ------ ..\p_appl\cfgsrv\pc_prc.c --------------
        @CC166 -I..\..\..\tools\complink\task_166\C166\include \
        -Id:\prj\jupiter\jupiter\src\imake\..\global \
        -Id:\prj\jupiter\jupiter\src\imake\.. \
        -Id:\prj\jupiter\jupiter\src\imake\..\config \
        -Id:\prj\jupiter\jupiter\src\imake\..\coreadap \
        -Id:\prj\jupiter\jupiter\src\imake\..\coreadap\bt_pt_sw \
        -Id:\prj\jupiter\jupiter\src\imake\..\coreadap\btos \
        -Id:\prj\jupiter\jupiter\src\imake\..\coreadap\btswte \
        -c -tmp -v -WaNOMOD166 -WaSN(REG172BT.DEF) -Ml -x -s -g -t \
        -T0 LISTALL EP DEBUG SG SN(REG172BT.DEF) NOMOD166 \
        -c ..\p_appl\cfgsrv\pc_prc.c -o ..\p_appl\cfgsrv\pc_prc.o66
        @del /F ..\p_appl\cfgsrv\pc_prc.erl
        @del /F ..\p_appl\cfgsrv\pc_prc.src
        @move pc_prc.erl ..\p_appl\cfgsrv
        @move pc_prc.src ..\p_appl\cfgsrv

Eine Erleichterung schaffen Make-Erweiterungen wie nmake, omake, clearmake usw., die eine weitergehende Konfigurierung der Makefiles ermöglichen.

Wir setzten ab 1996 unserer eigenes Maketool ein: SIEMAKE (Siemens Make), ein exakt auf die Bedürfnisse in der SW-Entwicklung für Gigasets (Schnurlose DECT Telefone) zugeschnittenes Make, das auch die Behandlung von Varianten unterstützt. Der Haken dieses Tools sind die in "C gegossenen" Rules, welche nicht so einfach erweitert und projektspezifisch angepasst werden können.

Makefile Generatoren

Einfacher gestaltet sich die Pflege der Makerules, wenn man Makefile Generatoren einsetzt, um im 1. Schritt Makefiles zu generieren, welche dann im 2. Schritt über den Aufruf von "make" das Programm erzeugen. Man gibt dann für jedes File eine Regel vor, die angewendet werden soll. Mit Generator Regeln definiert man dann für den Makefile Generator, wie die angegebenen Parameter in Makeanweisungen umgeschrieben werden sollen. Die Regeln für den Generator können zentral und getrennt von den Projektdaten erstellt und gepflegt werden. Ein weiterer Vorteil des Regelbasierten Verfahrens besteht darin, dass nur eine Regel geändert werden muss, um für 1000 oder mehr C-Files die Build-Rule im Makefile anzupassen. Unterschiedliche Build-Rules sind leicht an verschiedenen Generator Regeln zu erkennen.

Zusätzlich hat man die Möglichkeit, in den generierten Makefiles nach Fehlern zu suchen, mit neuen Rules zu experimentieren und die Compiler Flags zu überprüfen/anzupassen (vorausgesetzt, der Generator lässt keine Make Variablen stehen...).

Es existieren eine Vielzahl von Makefilegeneratoren, die alle ihre Stärken und Schächen haben und oft vom Configuration Management in Eigenregie aus der Not heraus entwickelt wurden (wie auch der EasyFileGenerator).

Perls MakeMaker

Perls "MakeMaker" wurde nicht als allgemeiner Makefilegenerator entworfen, siehe .../Perl/lib/ExtUtils/MakeMaker.pm:

This utility is designed to write a Makefile for an extension module from a Makefile.PL.

Imake

Imake z.B. verwendet den C-Preprozessor cpp und kann somit in den Rules nur Strings verketten. Zählungen der Files und der angewendeten Rules sind daher nicht möglich:

 SRC(File(dir_a\dir_af, file.c), Variants(HP800, SCO))

Wie bereits in der Einleitung beschrieben, führt eine vergessene schließende Klammer lediglich zu der Fehlermeldung:

 Error in line 12347: ")" missing...

Das Einzige, was hier weiterhilft, ist die "binäre" Suche nach der öffnenden Klammer: Etwas Code entfernen und prüfen, ob der Fehler noch auftritt. Wenn ja, im nächsten Schritt etwas mehr Code entfernen, wenn nein, etwas weniger Code entfernen. Mit Markierungen zwischen den Regeln lässt sich das Ganze auch mit Perl automatisieren, aber gerade das will der EasyFileGenerator ja vermeiden...

Einige wenige weitere Generatoren finden sich z.B. im Google Directory:

 Computers > Software > Build Management > Makefile Generators.

Diese Seite findet sich auch in den Goodies.

Probleme der mir bekannten Makefile Generatoren

Die Behandlung von unterschiedlichen Plattformen und Varianten für die zu generierenden Objects wird unübersichtlich bis problematisch, wenn bestimmte Files nur für bestimmte Varianten benötigt werden. Noch schwieriger ist es, projektspezifische Varianten von den Rules selbst zu erstellen.

Die meisten Makefile Generatoren verwenden eine eigene Syntax, sodass der Umstieg auf einen anderen Generator nur dann durchgeführt wird, wenn es unumgänglich ist. Denn zuerst muss der Configuration Manager die Syntax der neuen Regelsprache beherrschen, bevor er sich an die mühsame, zeitaufwendige und fehlerträchtige Aufgabe wagen kann, die Regeln umzustellen. Und zum Schluss müssen die Entwickler lernen, wie die neuen, andersartigen Regeln anzuwenden sind.

Da gab es soviele Möglichkeiten, etwas zu verbessern, dass ich gar nicht wusste, womit ich beginnen sollte. Um eine hohe Akzeptanz des neuen Tools zu erreichen, kümmerte ich mich zuerst um die "Anwenderschnittstelle", die Syntax der Rule-Calls.

Einfache Syntax

Gemäß der Entwicklungsphilosophie "Fortschritt ist der Weg vom Primitiven über das Komplizierte zum Einfachen" definierte ich zunächst eine sehr einfache Syntax für den Makefilegenerator, die sehr ähnlich zu der war, die bereits SIEMAKE verwendete:

 SRC   dir_a\dir_af  file.c   HP800|SCO

Lediglich ein & wurde von mir vorangestellt, damit man den Aufruf der Regeln leicht erkennen konnte:

 &SRC   dir_a\dir_af  file.c   HP800|SCO

Eine Regel fängt also immer mit & an und ist am Zeilenende auch zuende. Als Trenner zwischen den Argumenten werden (beliebig viele) Whitespaces, also Leerzeichen und Tabs, verwendet. Das ist deutlich übersichtlicher und einfacher als die Klammerung in Imake:

 SRC(File(dir_a\dir_af, file.c), Variants(HP800, SCO))

Wenn man den Call direkt in Perl absetzt, wird es auch nicht einfacher:

 &SRC( 'dir_a\dir_af',   'file.c',    'HP800|SCO' );

weil man berücksichtigen muss, dass viele der Anwender ( SW-Entwickler) mit Perl nicht vertraut sind und so besonders bei der Verwendung der Zeichen

 \ $ @ & % ( ) ' "

Probleme entstehen können.

1. Generation

Ausgehend von dieser Grundidee entwickelte ich eine eigene Syntax und einen Interpreter, der Variablen setzen, if-then-else-endif und ein paar Befehle mehr ausführen konnte. Allein das mehrstufige if-then-else-endif hat mich 4 Wochen Entwicklungsarbeit gekostet, weil das Tool, um Zeit zu sparen, immer nur den durch if selektierten Teil weiter geparst und den anderen Zweig überlesen hat (natürlich mussten if-then-else-endif weiterhin beachtet werden, damit auch das passende endif gefunden wurde).

Bald zeigte sich, dass die Syntax erweitert werden musste, um die Makefile Generierung durchführen zu können. Ausserdem wurde die Laufzeit mit der benötigten Stack-Tiefe (Calls in Calls in Calls...) deutlich größer. Und die Entwickler hatten keine Zeit, eine Spezialsprache zum Erstellen der Rules zu lernen, sodass ich selbst die meisten Make-Rules verfasste.

Doch schließlich lief der Generator, und ich konnte 2001 zum ersten Mal am "Deutschen Perl-Workshop" teilnehmen.

2. Generation

Am zweiten Tag des Workshops fiel mir auf, dass ich einfach nur die fehlenden Zeichen

 ('', "");

ergänzen musste, um ein Perl-Command zu erhalten:

 &SRC   dir_a\dir_af      file.c     HP800|SCO
 &SRC( 'dir_a\dir_af',   'file.c',  'HP800|SCO' );         # Perl !

Geleistet wird das ganze dann (für eine Zeile $l) durch den folgenden Code:

 if ($l =~ /\s*^(\#)/o) {
    # --- write # comment ---
    ...
 } elsif ($l =~ /^\s*&(\w+)\s*(.*)/o) {
    # &rule_call 1 2 3 4
    $rule   = $1;
    $argstr = "$2\n";
    
    if ($argstr =~ /^\(/io) {
        # &rule_call (1, 2, 3, 4);
        # --- rule startet with "(" --> standard perl --> just print out
        print OUTP $l;
        next;
    }
    while ($argstr) {
      if ($argstr =~ /^\'((?:[^\']|\'\')+)\'/o) {
         # --- 'String' recognizing, substituting '' to \' ---
         $pval   = $1;
         $argstr = $';
         $pval   =~ s/\'\'/\\\'/go; # ' # for emacs
         push (@cmdArr, "'$pval'");
      } elsif ($argstr =~ /^\"(([^\"]|\"\")+)(\")/o) {
         # --- "String" recognizing, substituting "" to \" ---
         $pval = $1;
         $argstr = $'; # ' #  for emacs
         $pval =~ s/\"\"/\\\"/go;
         push (@cmdArr, "\"$pval\"");
      } elsif ($argstr =~ /^([^\s\'\"]+)/o) {
         # --- handle string up to next white space, ' or " ---
         $pval = $1;
         $argstr = $'; # ' # for emacs
         push (@cmdArr, "'$pval'");
      } else {
         last;
      }
      $argstr =~ s/^\s+//o;
    }
 }
 $fh->write ("$sub (".join(', ', @cmdArr).");";

Das war der Start für die 2. Generation meines Makefile Generators. Die Regeln wurden ab jetzt in Perl geschrieben. Die Ausführung wurde dadurch deutlich beschleunigt: Der soeben erzeugte Perl-Code wurde einfach abgespeichert, per require eingezogen und ausgeführt. Ausserdem konnte ab jetzt jedes Perl-Kommando in den Makefiles verwendet werden, denn alle Zeilen, die nicht mit

 #
 //
 &

anfangen, wurden automatisch als Perl interpretiert und unverändert übernommen. Perl-Subs wurden von den Rules dadurch unterschieden, dass nach dem einleitenden &src noch eine Klammer auf "(" folgte:

 &src ( 123 ); # Perl
 &src   123    # kein Perl mit 7! Argumenten
 #       1     2  3    4    5  6     7

Zusätzlich gab es noch ein paar Spezialfunktionen, z.B.

 &include file.imk       # wie require, doch vorher parsen
 &setVar var dir1\dir2

und Variablen, die bereits vom Parser ausgewertet wurden

 &include $(var)\file.imk

Aber einige wichtige Punkte aus dem Anforderungskatalog fehlten noch, nämlich

 2      Varianten
 2.1      Konfigurierbar
 2.2      Alle auf einmal
 2.3      Einzeln
 3      Zielsysteme
 3.1      Konfigurierbar
 3.2      Alle auf einmal
 3.3      Einzeln
 3.4      (Compiler)
 8      Überlagerung von allg. Rules durch projektspezifische

Auch waren der Parser und der Generator noch nicht getrennt.

3. Generation

Der EasyFileGenerator implementiert schliesslich alle noch offenen Punkte des Anforderungskatalogs.

Die Syntax der Inputfiles bleibt fast unverändert:

 $g1->src    ( 123 ); # Perl
     &src -all 123    # kein Perl mit 8! Argumenten
 &include file.imk        # wie require, doch vorher parsen
 &setVar -all var dir1\dir2
 
 &include $(var)\file.imk

Beim Rule-Call und bei setVar wird ein weiteres Argument benötigt: Der Generator, der die Rule durchführen oder für den die Variable gesetzt werden soll.

Der Parser und die Generatoren sind als getrennte Objekte implementiert. Für jede Compilervariante kann eine eigene Generatorklasse von Efg::Generator abgeleitet werden, von der dann für jede Produktvariante eine Instanz mit eigenen Parametern versorgt werden kann. Vom Parser genügt eine Instanz, aber Ableitungen von Efg::Parser sind natürlich ebenfalls möglich. Der EasyFileGenerator zieht dann per require als erstes eine Datei (z.B. prepare.pl) ein, in der Parser und Generatoren erzeugt und mit Parametern versorgt werden. Dazu wird in dieser Datei die Funktion "&buildUpEfg" definiert, die von main zur Initialisierung aufgerufen wird:

 # prepare.pl
 use strict;
 use Efg;
 use Efg::Generator::HP800Compiler;
 use Efg::Generator::SCOCompiler;
 use Efg::Generator::WinCC;
 use Efg::Generator::MSDevStudio;
 use Efg::Generator::Xml;
 use Efg::Parser::MyParser;
 # --- will be called by main to setup the Efg and the generators ---
 sub buildUpEfg {
    my $efg = new Efg;
    $efg->setParser(new Efg::Parser::MyParser());
    my $g1 = new Efg::Generator::HP800Compiler();
    my $g2 = new Efg::Generator::SCOCompiler();
    my $g3 = new Efg::Generator::WinCC();
    my $g4 = new Efg::Generator::MSDevStudio();
    my $g5 = new Efg::Generator::Xml(); # *.c-Files only
    my $g6 = new Efg::Generator::Xml(); # all files
    $g1->Config(OUTPUT_DIR => "$CFG{OUTPUT_DIR}\\g1",
                BLUBB      => 'blubber');
    $g5->Config(FILE_TYPE  => '*.c'); # *.c-Files only
    $g1->outputFile('HP800.mk');
    $g2->outputFile('SCO.mk');
    $g3->outputFile('WinCC.mk');
    $g4->outputFile('DevStudio.prj');
    $g5->outputFile('files_c.xml');
    $g6->outputFile('files_all.xml');
    $efg->addGenerator('HP800', $g1);
    $efg->addGenerator('SCO', $g2);
    $efg->addGenerator('WinCC', $g3);
    $efg->addGenerator('MSDev', $g4);
    $efg->addGenerator('xml.c', $g5);
    $efg->addGenerator('xml.all', $g6);
    return $efg;
 }
 1;

Es werden sechs Dateien generiert, drei Makefiles für HP800, SCO (beide Unix) und WinCC, eine Projektdatei für das Microsoft Developer Studio und zwei XML Dateien.

Aus jedem Rulecall werden jetzt bis zu sechs Generator Calls:

 &SRC -all a2 a3 a4

wird zu

 $g1->SRC('-all', 'a2', 'a3', 'a4');
 $g2->SRC('-all', 'a2', 'a3', 'a4');
 $g3->SRC('-all', 'a2', 'a3', 'a4');
 $g4->SRC('-all', 'a2', 'a3', 'a4');
 $g5->SRC('-all', 'a2', 'a3', 'a4');
 $g6->SRC('-all', 'a2', 'a3', 'a4');

und

 &SRC  HP800|SCO   b2 b3 b4

wird zu

 $g1->SRC('HP800|SCO', 'b2', 'b3', 'b4');
 $g2->SRC('HP800|SCO', 'b2', 'b3', 'b4');

Aufgerufen wird der EasyFileGenerator schliesslich mit:

 perl -w efg.pl --config ../src/config.pl

Die Datei config.pl enthält die Voreinstellungen des EasyFileGenerators:

 # cfg.pm
 use strict;
 use CFG;
 $CFG{"INPUT_DIR"}    = '../src';
 $CFG{"OUTPUT_DIR"} = '../gen';
 $CFG{prepare_pl}     = 'prepare.pl';
 $CFG{genRules_pm}    = 'genRules.pm';
 $CFG{start_efg}      = 'start.efg';
 1;

diese können durch Shell Variablen

 SET EFG_VARIANT=A

und diese wiederum durch Aufrufparameter --data VAR=VALUE überschrieben werden:

 perl -w efg.pl --data OUTPUT_DIR=../gen --config ../src/config.pl

Man beachte das den Shell Variablen vorangestellte "EFG_", welches die Eindeutigkeit der Namen garantiert und vor der Speicherung der Variablen im Hash "%CFG" entfernt wird. Die Shell Variable "EFG_VARIANT" wird demnach unter dem Key "VARIANT" im Hash abgelegt.

 $CFG{prepare_pl}

enthält den Dateinamen mit der oben angegebene Startup Funktion, und

 $CFG{genRules_pm}

definiert die Rules für die einzelnen Generatoren. Ein

 use Efg::Generator::GenHP800;
 use Efg::Generator::GenSCO;
 use Efg::Generator::GenWinCC;

ist ausreichend (vorausgesetzt, man hat die Klassen irgendwo definiert, wo perl sie auch finden kann...). Noch einfacher kann man auch die Regeln direkt für Efg::Generator definieren:

 # genRules.pm
 #
 # Rules for the standard generator
 use strict;
 package Efg::Generator;
 sub SRC {
    my $self = shift;
    my @parArr = $self->parseForAndInsertVariables(@_);
    # print join ("\t", $self->{_fh}, @parArr)."\n";
    $self->write(join ("\t", @parArr)."\n");
 }

1;

Argumente mit White Space, $( , ' oder "

Enthalten die Parameter der Regel "White-Space" Zeichen, $( , ' oder ", werden folgende Escape Sequenzen verwendet:

 1: &SRC   'dir_a\dir_af'      "file"".c"     HP800|SCO
 2: &SRC   'Program Files'     'file''A.c'    HP800|SCO
 3: &SRC   $$(dir)             fileA.c        HP800|SCO
 4: &SRC   '$(dir)'            fileA.c        HP800|SCO

Innerhalb von '...' wird ' durch Verdopplung escaped, also ' ' statt ' verwenden (2). " wird innerhalb "..." ebenso escaped (1). Ein $ wird ebenfalls mit sich selbst escaped (3), d.h. $$ -> $. White Space Zeichen müssen von '...' oder "..." umschlossen sein (2), um nicht als Parametertrenner angesehen zu werden. "..." und '...' (!) Strings werden dann vom Generator nach Variablen $(Abc1) durchsucht (4) und die Variable durch den Wert von

 $self->{"_var_Abc1"}

ersetzt. Dabei muss man berücksichtigen, dass Perl den String

 "$val \t\n"

ebenfalls wie gewöhnlich interpretiert (also einen Tab und ein Newline einfügt sowie den Wert der Perl-Variablen $val) und

 '$val \t\n'

unverändert an die Rule weitergegeben wird.

Variablen

Variablen werden durch

 &setVar -all var1 dir1\dir2

gesetzt und der Wert kann mit

 $(var1)

wieder abgerufen werden. Der erste Parameter von setVar gibt an, für welchen Generator die Variable gesetzt werden soll.

 -all: # Variable für alle Generatoren setzen.
 HP800|SCO # Variable für die Generatoren setzen, die unter den Namen
           # HP800 bzw. SCO bekannt sind.

Der Parser wandelt dann die Regel setVar genauso wie alle anderen auch in Perl-Calls um:

 $g1->setVar(  'var1', 'dir1\dir2');
 $g2->setVar(  'var1', 'dir1\dir2');
 $g3->setVar(  'var1', 'dir1\dir2');
 $g4->setVar(  'var1', 'dir1\dir2');
 $g5->setVar(  'var1', 'dir1\dir2');
 $g6->setVar(  'var1', 'dir1\dir2');

Diesen Mechanismus zum Setzen von Variablen könnte man auch zur Steuerung des Parsers verwenden, aber bisher habe ich das noch nicht benötigt.

Spezielle Regeln

&include

Die Regel

 &include inc.efg

in der Datei start.efg weist den Parser an, die Datei "inc.efg" zu parsen. Das Ergebnis "inc.efg.pl" wird als require in die Perl Ergebnisdatei start.efg.pl geschrieben:

 require '../gen/inc.efg.pl';

Das '../gen/' wird durch die Generator Variable "OUTPUT_DIR" erzeugt.

&fix_text

Sollen mehrere Zeilen unverändert in die Ausgabedatei (Makefile) übernommen werden, kann man das einfach mit

 &fix_text HP800|SCO << EnDe
 Das ist ein kurzer Test Text.
 This is a short test text.
 Das ist ein weiterer kurzer Test Text.
 This is another short test text.
 Das ist der letzte kurze Test Text.
 This is the last short test text.
 EnDe

erledigen. Die Generatoren für "HP800" und "SCO" schreiben dann den ganzen Text unverändert ab der nächsten Zeile bis zum Ende Tag,

 EnDe

das nach dem << angegeben wurde und am Zeilenanfang stehen muss. Das Leerzeichen als Trenner zwischen << und Ende Tag ist zwingend vorgeschrieben. (Im Gegensatz zu den UNIX Shells, die dort kein Leerzeichen erlauben.)

Überlagerung von Regeln

Projektspezifische Überlagerung von Regeln wird einfach durch Ableitung des Generators durchgeführt. Die abweichenden Regeln können dann in der Unterklasse neu definiert werden. In der Funktion "&buildUpEfg" muss dann nur eine Instanz der Subklasse erzeugt und mit dem richtigen Namen versehen werden.

Die Software

Dank Perl ist es mir gelungen, den EasyFileGenerator mit 7 Dateien und insgesamt weniger als 20 KB Speicherplatz zu implementieren, die Rules nicht mitgerechnet. Die Sourcen finden sich, zusammen mit einer Testumgebung, unter den Goodies auf der Workshop CD und demnächst auf meiner Homepage http://www.jupiter-programs.de. Die Veröffentlichung der SW auf dem CPAN kann erst dann erfolgen, wenn alle Bedingungen für das CPAN erfüllt sind, was wohl noch ein wenig dauern wird.

Known Bugs

Nicht existierende Zielverzeichnisse werden noch nicht durch die Generatoren angelegt.

Ausblick: weitere Anforderungen

Im folgenden möchte ich noch ein paar neue Anforderungen beschreiben, auf die ich während der Implementierung gestoßen bin.

Newline

Die verschiedenen Zeichenfolgen für Newline in MSWindows (#0D0A) und LINUX (#0A) müssen im Generator und im Parser konfigurierbar werden. Bis dahin kann der Generator (für ein Projekt) entweder auf UNIX oder auf MSWindows gestartet werden, weil es Probleme mit dem Matching von <newline> geben kann.

Verzeichnistrenner

Als Verzeichnistrenner fungieren zur Zeit \ und /, alle \ werden für Perl in / umgewandelt. Das kann für Calls unter MS-Windows problematisch werden, da hier die Command Options oft mit vorangestelltem / und nicht mit - eingeleitet werden. Andererseits ist die Verwendung von \ in Perl umständlich und fehleranfällig, besonders in Strings der Art "Text". Falls jemand dafür eine "easy" Lösung findet, nur her damit!

Auswahl des Generators

Die Auswahl der Generatoren, die die Rule ausführen sollen, wird zur Zeit fest an den ersten Parameter gebunden. In einer der nächsten Versionen können die Parameter (1 bis n), die vom Parser zur Auswahl der Generatoren verwendet werden sollen, noch global und für jede Rule einzeln eingestellt werden. Eine "easy" Lösung ist mir dafür leider noch nicht eingefallen.

File Stack für Generatoren

Es könnte erforderlich sein, von einem Generator mehrere Ausgabedateien erstellen zu lassen. Dazu möchte ich einen "File Stack" realisieren, der das aktuelle File schliesst, das nächste öffnet und nach Fertigstellung wieder auf das alte File zurückschaltet.

 # schreibe in file Makefile.HP800
 #  rules, rules, rules ...
 # schreibe ab jetzt in "dasIstEinNeuesMakefile.mk"
 &startFile HP800 dasIstEinNeuesMakefile.mk
 # rules, rules, rules ...
 &endFile HP800
 # schreibe wieder in file Makefile.HP800

Generierung in Verzeichnisbäumen

In größeren Systemen, die aus vielen zu generierenden Komponenten (Programmen u.ä.) bestehen, sollte man den EasyFileGenerator für das Gesamtsystem, ein beliebiges Subsystem oder nur für eine einzelne Komponente starten können. Auch dafür suche ich noch nach einer "easy" Lösung.

Abschluss

Ich hoffe, dass der eine oder andere sich an den EasyFileGenerator erinnert, wenn es daran geht, Makefiles zu generieren. Viele andere Wahlmöglichkeiten (noch dazu kostenfreie) bieten sich nicht an, Google hat lediglich fünf Makefilegeneratoren im Angebot (MakeMaker befindet sich merkwürdigerweise nicht darunter). In den vielen anderen von mir durchsuchten Web-Verzeichnissen habe ich bisher keine weiteren Generatoren finden können.

Anregungen, Fehlermeldungen und Erweiterungswünsche zum EasyFileGenerator sind immer willkommen und natürlich leiste ich auch Hilfestellung bei der Anwendung / Einführung.

Weitergehende Dokumentation und Tutorials werde ich (hoffentlich bald) auf meiner Homepage veröffentlichen.

Ralf Peine, im Mai 2004

"Perl: every time my favorite camel"

Bibliographie

German Perl Workshop.
German Perl Workshop Website. http://www.perlworkshop.de

Google Directory:
Computers > Software > Build Management > Resources

siehe Goodies u. http://directory.google.com/Top/Computers/Software/Build_Management

Make - A Program for Maintaining Computer Programs
S. I. Feldman, Bell Laboratories Murray Hill, New Jersey 07974 http://citeseer.nj.nec.com/feldman79make.html

OMAKE Guide
Rational ClearCase (gehört seit einem Jahr zu IBM)