Automatische Builds mit Visual Studio und Source Safe


Thomas Wölfer
Thomas Wölfer

22. November 2007


Warum überhaupt automatische Builds? Kurze Antwort: Weil man sonst nicht weiss, ob man sein Projekt überhaupt bauen kann. Der Grund dafür ist der, das das Visual Studio einige "magische" Dinge tut die dazu führen können, das man ein Projekt bauen kann, obwohl - wenn alles "ordnungsgemäß" ablaufen würde - der Build fehlschlagen müsste.

Ich habe dafür ein paar Beispiel, die in unserer aktuelle Solution tatsächlich aufgetreten sind. Die Solution umfasst zur Zeit 65 Projekte. 2 davon sind Setup-Projekte, die anderen sind C# sowie Native und Manged C++ Projekte. Ein Projekt enthält überhaupt keine Code, sondern ausschliesslich Dokumentation in Form von HTML, CSS, JavaScript und Bitmaps.

Problemfälle die aufgetreten sind:

  • Ein (C#) Projekt konnte (innerhalb von VS) gebaut werden, obwohl es einen Typ benutzte der aus einer Assembly stammte, die nicht referenziert wurde. (Der Build auf der Kommandozeile schlug fehl.)
  • Ein (Managed C++) Projekt konnte gebaut werden, obwohl im Code ein Typ aus einer nicht referenzierten Assembly verwendet wurde. (Der Build auf der Kommandozeile schlug fehl.)
  • Einige C++ Projekte konntn gebaut werden, obwohl die Projekt-Abhängigkeiten (und damit die Build-Reihenfolge) nicht korrekt war.

Der eigentliche Grund warum wir regelmäßige "nächtliche" Builds auf einem Buildserver durchführen wollten war aber ein anderer: Wir haben einen mitlerweile recht großen Satz an Testcases, bei denen es aufgrund des Umfangs langsam aber sicher unhandlich wird, sie interaktiv durchlaufen zu lassen. Statt dessen werden dieses Testläufe nun einfach automatisch nachts durchgeführt - schlägt ein Test fehl, werden die Entwickler per eMail informiert. Praktische Sache.

Nun sollte man annehmen, das automatische Builds eine einfach Sache sein sollten - schließlich ist das Projektsystem seit Visual Studio 2005 ja auf MSBuild aufgebaut: Zumindest hiess es das immer. In der Theorie sollte es also reichen

MSBuild NameDerSolution

einzugeben, und MSBuild sollte dann genau das gleiche Produzieren, was auch innerhalb der IDE stattfindet. Nun - das ist leider nicht so, wie ich feststellen musste. Doch eins nach dem anderen: Das erste Problem war nämlich, das vor jedem Build zunächst einmal ein aktueller Satz an Quellcode-Dateien aus dem Revision Control System besorgt werden muss. Wir verwenden als RCS Visual Source Safe (*1), und also machte ich mich auf die Suche in der zugehörigen Dokumentation.

Die ist, um es höflich zu sagen, etwas mager. Um per Kommandozeile ein Get-Latest-Version zu machen, muss man folgendes tun:

  • Die Environment-Variable "ssuser" auf einen gültigen Source Safe Usernamen setzen.
  • Die Environment-Variable "sspwd" auf das zugehörige Passwort setzen.
  • Die Environment-Variable "SSDIR" auf das Source-Safe Verzeichnis der Solution setzen.
  • Per "cd" in das Verzeichnis wechseln, in das Source Safe die lokale Kopie des Codes platzieren soll. (Es gibt eine Option mit der man dieses Verzeichnis angeben kann, die habe ich aber nicht ans laufen gebracht.)
  • Dann per "ss" Kommando die neueste Version holen: ss get $/PfadZurSolutionInderSSDatenbank -R
  • Dann bekommt man das Problem, das Source Safe als erstes einen Prompt ausgibt und fragt, ob das aktelle Verzeichnis das permanente Default-Verzeichnis für die Solution werden soll. Mit dieser Frage hatte ich zwei Probleme: 1) Ich verstehe sie nicht und 2) Für ein automatisches Build ist sie hinderlich. Nun gibt es eine Option "-Y", mit der man angeblich alle interaktiven Prompts abschalten kann - aber leider weiss ich nicht, ob die Antwort auf die Frage dann "Ja" oder "Nein" lautet - und außerdem habe ich die Option ausprobiert: Die Frage wurde aber trotzdem nicht unterdrückt. Daher Rückgriff auf MS-DOS Zeiten:
  • Anlegen einer Datei "N.dat", die ausschließlich den Buchstaben N enthält und abändern des SS-Aufrufs auf:
  • ss $/PfadZumProjekt -R <n.dat

Damit hat man zumindest schon mal den aktuellen Quellcode. Nun gehts ans bauen. Nichts einfacher als das, dachte ich:

msbuild NameDerSolution.sln /t:Rebuild /p:Configuration=Release

In der Theorie ist das auch richtig - aber in der Praxis ging das bei mir nicht.

  • msbuild kann keine Setup-Projekte bauen
  • msbuild kann auch keine C++ Projekte bauen, und delegiert die Arbeit dazu an ein anderes Tool. Das übersetzt dann in der Theorie auch richtig - nur leider klappte das auch nicht.

Unabhängig von den fehlenden Setup-Projekten (damit könnte ich leben), habe ich zwei Probleme gefunden:

1) Wenn man in einem Managed C++ Projekt eine Referenz auf ein anderes Projekt mit Managed Code hat, dann wird das von MSBuild völlig ignoriert. Die Referenz müsste eigentlich in die Compiler-Option "/FU PfadZurAssemblyDerReferenz" umgesetzt werden. Das passiert aber nicht. Resultat: Der Compiler kennt die aus der referenz stammenden Typen (und Namespaces) nicht, und der Build schlägt fehl. Das kann man allerdings umgehen, indem man nicht das Projekt referenziert, sondern die Assembly per "#using <Pfad zur Assembly>" einbettet: Nicht schön, funktioniert aber.

2) Ich hatte einen Fall (den ich auch immer wieder reproduzieren kann, aber leider nur mit genau diesem Projekt), bei dem im Build auf der Kommandozeile eines C# Projektes eine gesetzte Referenz ebenfalls einfach ignoriert wurde. Warum das in diesem Fall passierte konnte ich nicht herausfinden, und einen Workaround dafür habe ich auch nicht. (In allen anderen Fällen funktionierte das ja auch, nur in einem einzelnen Fall eben nicht. Das ist aber genauso gut, als wenn es nie gehen würde: Der Build schlägt fehl.)

Also kam MSBuild leider nicht in Frage. Netterweise gibt es aber eine Alternative: Visual Studio hat auch einen Kommandozeilen-Modus, mit dem man Builds durchführen kann. Der Aufruf dazu geht wie folgt:

devenv PfadZumSolutionFile.sln /Rebuild "Debug" /out PfadZuEinemLogFile.log

Ruft man das aber so auf, dann passiert folgendes: C++ Build schlagen fehl. Grund: Include- und Lib-Dateien werden nicht gefunden. In der Theorie ist es so, das man über die VC++ Projektoptionen in der IDE die richtigen Pfade für Header und Libraries einstellt, und das die Kommandozeilen-Variante dann diese Einstellungen verwendet. Für Header hat das bei mir auch geklappt, für Libraries aber nicht.

Workaround: Vor dem Aufruf von "devenv" die Environment-Variablen "INCLUDE" und "LIB" mit den Pfaden zun den Headern und Libraries setzen, und "devenv" dann mit der zusätzlichen Option "/UseEnv" aufrufen.

--

(*1) Mir ist bewusst das Source Safe keinen besonders guten Ruf geniesst, und ich bin auch mit einigen Dingen darin eher unzufrieden. Unabhängig davon kann ich aber die Meldungen über "verlorerne" Daten, die immer wieder gern angebracht werden, absolut nicht bestätigen.