Globale SVN-Revisionsnummer im Projekt - und zwar richtig!

Wenn man ein Projekt aufsetzt, dann hat man meist eine Versionsnummer im Format a.b.c.d, mit
  1. a = Hauptversionsnummer, ändert sich eigentlich nur bei massiven Änderungen
  2. b = zeigt an, dass neue Features hinzugekommen sind
  3. c = zeigt an, dass etwas gepatched bzw. geupdated wurde
  4. d = gibt den Revisionsstand der Software im Versionsverwaltungssystem an.
Bei SVN gibt es die Möglichkeit sich per Code-Kommentar eine Revisionsnummer direkt in den Quelltext schreiben zu lassen. Das sieht dann beispielsweise so aus:
  1. /*!
  2. *******************************************************************************
  3. * File identification: $Id:$
  4. * Revision of last commit: $Rev:$
  5. * Author of last commit: $Author:$
  6. * Date of last commit: $Date:$
  7. *******************************************************************************
  8. */
Und wird von SVN beim Commit ersetzt zu
  1. /*!
  2. *******************************************************************************
  3. * File identification: $Id: main.cpp 178 2016-02-08 14:42:36Z cypax $
  4. * Revision of last commit: $Rev: 178$
  5. * Author of last commit: $Author: cypax $
  6. * Date of last commit: $Date: 2016-02-08 15:42:36 +0100 (Mo, 08 Feb 2016) $
  7. *******************************************************************************
  8. */
Vorausgesetzt natürlich, dass die Datei die entsprechenden SVN-Keywords gesetzt hat:
SVN keywords

So weit, so gut. In dem Beispiel oben ist die Revisionsnummer 178. Wenn es jetzt aber noch eine zweite Datei gibt und Änderungen an dieser comitted werden, dann bleibt in main.cpp die 178 stehen. Warum? Weil die SVN-Revisionsnummer dateibezogen ist - nicht projektbezogen! Das ist ja jetzt nicht ganz das, was wir haben wollten. Zum Glück, gibt es aber ein Tool, welches genau für unsere Zwecke gedacht ist: svnversion! Um eine globale Revisionsnummer zu erhalten erstellt man eine leere Datei revision.h, checkt sie nicht in SVN ein und schreibt folgendes in die *.pro-Projektdatei eines Qt-Projekts:
  1. PRE_TARGETDEPS += $$PWD/code/revision.h
  2. # Obtain SVN revision
  3. SVN_REVISION = $$system(svnversion -n)
  4. # split into list by ':'
  5. REVISION_LIST = $$split(SVN_REVISION,:)
  6. # get last item of list
  7. HEAD_REVISION = $$last(REVISION_LIST)
  8. # remove M (modified working copy)
  9. HEAD_REVISION = $$replace(HEAD_REVISION,M,)
  10. # remove S (switched working copy)
  11. HEAD_REVISION = $$replace(HEAD_REVISION,S,)
  12. # remove P (partial working copy, from a sparse checkout)
  13. HEAD_REVISION = $$replace(HEAD_REVISION,P,)
  14. QMAKE_EXTRA_TARGETS += revtarget
  15. revtarget.target = $$PWD/code/revision.h
  16. unix {
  17. revtarget.commands = "$$system(echo \'/* generated file - do not edit */\' > $$revtarget.target)"
  18. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}ifndef REVISION_H\' >> $$revtarget.target)"
  19. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define REVISION_H\' >> $$revtarget.target)"
  20. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define SVN_REVISION \"$$SVN_REVISION\"\' >> $$revtarget.target)"
  21. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define HEAD_REVISION $$HEAD_REVISION\' >> $$revtarget.target)"
  22. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define HEAD_REVISION_STRING \"$$HEAD_REVISION\"\' >> $$revtarget.target)"
  23. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}endif // REVISION_H\' >> $$revtarget.target)"
  24. }
  25. win32 {
  26. revtarget.commands = "$$system(echo '/* generated file - do not edit */' > $$revtarget.target)"
  27. revtarget.commands += "$$system(echo '$${LITERAL_HASH}ifndef REVISION_H' >> $$revtarget.target)"
  28. revtarget.commands += "$$system(echo '$${LITERAL_HASH}define REVISION_H' >> $$revtarget.target)"
  29. revtarget.commands += "$$system(echo '$${LITERAL_HASH}define SVN_REVISION \"$$SVN_REVISION\"' >> $$revtarget.target)"
  30. revtarget.commands += "$$system(echo '$${LITERAL_HASH}define HEAD_REVISION $$HEAD_REVISION' >> $$revtarget.target)"
  31. revtarget.commands += "$$system(echo '$${LITERAL_HASH}define HEAD_REVISION_STRING \"$$HEAD_REVISION\"' >> $$revtarget.target)"
  32. revtarget.commands += "$$system(echo '$${LITERAL_HASH}endif // REVISION_H' >> $$revtarget.target)"
  33. }
  34. revtarget.depends = FORCE
  35. QMAKE_DISTCLEAN += $$revtarget.target
Zur Erklärung: Als erstes wird qmake mitgeteilt, dass es eine zusätzliche Abhängigkeit gibt (PRE_TARGETDEPS, Zeile 1) - nämlich besagte revision.h-Datei. Dann werden ein paar Variablen mit den SVN-Revisionsinformationen gefüllt. Mit dem Kommando svnversion -n (Zeile 4) erhält man die SVN-Revisionsnummer. Der Parameter -n bewirkt, dass die Ausgabe keinen Zeilenumbruch enthält. Die Variable SVN_REVISION enthält somit die Ausgabe von svnversion, ausgehend vom Pfad, in der die Projektdatei liegt. Wenn die lokale Arbeitskopie allerdings modifiziert, unvollständig ausgecheckt oder zu einer anderen Revision geswitched wurde, ist die Revisionsnummer nicht einfach eine Zahl, sondern nach dem Schema [abc:]xyz[M|S|P] aufgebaut, wobei xyz die Headrevision ist (für Details einfach mal in der Konsole svnversion -help eingeben). Aus diesem Grund teilen wir die Ausgabe anhand des Trennzeichens ":" auf (Zeile 6), nehmen das letzte Element dieser Liste (Zeile 8) und entfernen alle M-, S- und P-Zeichen (Zeilen 10 - 14). In Zeile 16 wird qmake mitgeteilt, dass es ein zusätzliches Target revtarget gibt (QMAKE_EXTRA_TARGETS), welches die, im Unterordner /code befindliche, revision.h-Datei ist und welche stets neu zu erstellen ist (Zeile 39). Das Befüllen der revision.h erfolgt in den Zeilen 19 - 37. Zu beachten ist, dass die erste echo-Ausgabe mit einem einfachen > umgeleitet wird. Dadurch wird der der Inhalt der Datei überschrieben. Anhängen weiterer Zeilen erfolgt mit >>. Die echo-Anweisungen für Unix müssen übrigens deshalb extra escaped werden, weil ein echo /* generated file - do not edit */ erst alle Dateien unter / auflisten würde, dann "generated file - do not edit" ausgibt und dann alle Dateien im aktuellen Verzeichnis auflistet.