Der Vorteil von Systemen wie CruiseControl oder Hudson ist unumstritten. Durch die Continuous Integration bleibt der Code-Stand eines Projektes ständig getestet. Das ermöglich das kontinuierliche Bauen von neuen Versionen, die, zumindest was die automatisierten Tests betrifft, lauffähig sind.
Sobald ein Build fehlschlägt erhält der Autor eine E-Mail bezüglich des Fehlers - und kann diesen zeitnah wieder beheben.
Dieser Artikel beleuchtet, wie man den Kajona Entwicklungsstand automatisiert bauen kann. Die Adaption auf eigene Projekte ist dabei recht einfach zu bewerkstelligen.
Als Vorraussetzung gilt hier ein lauffähiges PhpUnderControl, eine Installationsanleitung findet sich auf deren Projektwebsite. PhpUnderControl setzt auf CruiseControl auf, die Installation ist aber ebenfalls bei der Installation von PhpUnderControl beschrieben.
Jedes Projekt wird innerhalb von CruiseControl auf Basis einer build.xml Datei generiert. Diese Datei besteht aus ANT-Tasks, die für das Bauen und Testen des Projektes verantwortlich sind. Besonders im Umfeld von Java hat sich ANT etabliert, wie sich hier zeigt lassen sich jedoch auch andere Projekt-Arten wie PHP-Projekt damit bauen und konfigurieren.
Die im Nachfolgenden vorgestellte build.xml kann als Template für Kajona-Projekte dienen.
<?xml version="1.0" encoding="UTF-8"?>
<!-- ======================================================================
10.02.2011 22:03:21
Kajona Head Build
Buildfile to check out the lastest Kajona version from SVN, build a project and install it.
Performes additional actions such as phpunit-tests, phpdoc generation, ...
sidler
====================================================================== -->
<project name="Kajona Head Build" default="build" basedir=".">
<description>
Buildfile to check out the lastest Kajona version from SVN, build a project and install it.
</description>
<!--
CONFIG VALUES BELOW
-->
Zum Bau und Check-Out des Projektes werden verschiedene Ordner benötigt. Diese werden im Kopf-Bereich der Datei einmalig definiert und dann innerhalb der Tasks wiederverwendet.
<!-- target dir for the sources from the svn repo -->
<property name="sourcesPath" value="/sources" />
<!-- target dir to install the system to -->
<property name="projectPath" value="/project" />
<!-- artifacts such as logs and reports will be stored here -->
<property name="buildPath" value="/build" />
<!-- generated zips will be placed here (full, light) -->
<property name="packagesPath" value="/packages" />
<!-- temp-folder, required to build the packages -->
<property name="tempPath" value="/temp" />
Für das Update der Sourcen ist der Zugang zum SVN Repository notwendig.
<property name="svnPath" value="https://svn.kajona.de/svn/kajona/kajona/module_head/trunk/" />
<property name="svnUsername" value="guest" />
<property name="svnPassword" value="guest" />
<!--
No need to change anything below!
-->
Der Init-Task initalisiert die Projektumgebung. So wird u.A. die Revsions-Number des aktuell zu Bauenden Systems aus SVN ermittelt.
<!-- internal initialization -->
<target name="init">
<tstamp />
<exec executable="svn" output="${basedir}${buildPath}/svn.properties">
<arg line="info '${basedir}${sourcesPath}'" />
</exec>
<property prefix="svn" file="${basedir}${buildPath}/svn.properties"/>
<echo>Revision found: ${svn.Revision}</echo>
</target>
Der Build-Task dient als Klammer. Er ruft alle einzelnen Tasks auf, die für einen vollständigen Bau des Projektes benötigt werden.
<target name="build" depends=" init,
cleanFilesystem,
update,
buildProject, installProject,
runPhpUnitTests, runPhpMD, runPhpCodeSniffer, runPhpCPD, runPDepend, generatePhpDocs,
buildFullZip, buildLightZip" />
Das Löschen von bestehenden temporären Dateien (z.B. vom vorhergegangenen Build-Lauf) erfolgt im Task cleanFilesystem.
<!-- removes existing folders and creates them again -->
<target name="cleanFilesystem">
<delete dir="${basedir}${packagesPath}"/>
<mkdir dir="${basedir}${packagesPath}"/>
<delete dir="${basedir}${tempPath}"/>
<mkdir dir="${basedir}${tempPath}"/>
<delete dir="${basedir}${projectPath}"/>
<mkdir dir="${basedir}${projectPath}"/>
<delete dir="${basedir}${buildPath}"/>
<mkdir dir="${basedir}${buildPath}"/>
<mkdir dir="${basedir}${buildPath}/logs"/>
<mkdir dir="${basedir}${buildPath}/coverage"/>
<mkdir dir="${basedir}${buildPath}/api"/>
<mkdir dir="${basedir}${buildPath}/graph"/>
</target>
Checkout und Update sollten selbsterklärend sein - die Sourcen werden auf einen aktuellen Stand gebracht.
<!-- performes a complete checkout from svn -->
<target name="checkout">
<delete dir="${basedir}${sourcesPath}"/>
<mkdir dir="${basedir}${sourcesPath}"/>
<exec executable="svn">
<arg value="checkout" />
<arg value="${svnPath}" />
<arg value="${basedir}${sourcesPath}" />
<arg line=" --username ${svnUsername} --password ${svnPassword} --non-interactive" />
</exec>
<!-- retrigger svn rev -->
<antcall target="init" />
</target>
<!-- updates the sources from svn -->
<target name="update">
<exec executable="svn">
<arg value="update" />
<arg value="${basedir}${sourcesPath}" />
<arg line=" --username ${svnUsername} --password ${svnPassword} --non-interactive" />
</exec>
<!-- retrigger svn rev -->
<antcall target="init" />
</target>
BuildProject erstellt aus der SVN-Struktur ein lauffähiges System. Dafür werden die Inhalte der einzelnen Module in einen gemeinsamen Zielordner (und damit zu einem lauffähigen System) kopiert.
<!-- builds the project, aka creates a project out of the sources -->
<target name="buildProject">
<copy todir="${basedir}${projectPath}" overwrite="true" >
<fileset id="sources.dirs" dir="${basedir}${sourcesPath}">
<exclude name=".svn"/>
<include name="element_*/**"/>
<include name="modul_*/*/**"/>
<include name="widget_*/*/**"/>
<include name="template_*/*/**"/>
<include name="_debugging/*/**"/>
</fileset>
<mapper>
<!-- strip first directory -->
<regexpmapper handledirsep="yes" from="[_a-z0-9]+/(.*)" to="\1" />
</mapper>
</copy>
</target>
BuildFullZip sowie BuildLightZip erstellen aus den Quellen fertige ZIP-Pakete. Diese können direkt entpackt und installiert werden. Der Dateiname beinhaltet dabei sowohl Datum, als auch Revision des Build-Laufes.
<!-- creates the full-zip including all modules and elements -->
<target name="buildFullZip" depends="init" >
<delete dir="${basedir}${tempPath}"/>
<mkdir dir="${basedir}${tempPath}"/>
<copy todir="${basedir}${tempPath}" overwrite="true" >
<fileset id="sources.dirs" dir="${basedir}${sourcesPath}">
<exclude name=".svn"/>
<include name="element_*/**"/>
<include name="modul_*/*/**"/>
<include name="widget_*/*/**"/>
<include name="template_*/*/**"/>
<include name="_debugging/*/**"/>
</fileset>
<mapper>
<!-- strip first directory -->
<regexpmapper handledirsep="yes" from="[_a-z0-9]+/(.*)" to="\1" />
</mapper>
</copy>
<zip destfile="${basedir}${packagesPath}/kajona_full_rev${svn.Revision}_${DSTAMP}-${TSTAMP}.zip">
<fileset dir="${basedir}${tempPath}" />
</zip>
</target>
<!-- creates the light-zip, only a limited set of modules included -->
<target name="buildLightZip" depends="init">
<delete dir="${basedir}${tempPath}"/>
<mkdir dir="${basedir}${tempPath}"/>
<copy todir="${basedir}${tempPath}" overwrite="true" >
<fileset id="sources.dirs" dir="${basedir}${sourcesPath}">
<exclude name=".svn"/>
<include name="modul_navigation/*/**"/>
<include name="modul_pages/*/**"/>
<include name="modul_samplecontent/*/**"/>
<include name="modul_system/**"/>
</fileset>
<mapper>
<!-- strip first directory -->
<regexpmapper handledirsep="yes" from="[_a-z0-9]+/(.*)" to="\1" />
</mapper>
</copy>
<zip destfile="${basedir}${packagesPath}/kajona_light_rev${svn.Revision}_${DSTAMP}-${TSTAMP}.zip">
<fileset dir="${basedir}${tempPath}" />
</zip>
</target>
InstallProject führt die einzelnen Installer des Systems aus. Damit wird das System lauffähig. Die Installation erfolgt dabei über ein externes PHP-Skript.
<!-- triggers the installation of the project aka. creating the db-structure -->
<target name="installProject">
<echo>Creating full Kajona installation</echo>
<exec executable="php" dir="${basedir}" failonerror="on" >
<arg line="-f buildProject.php ${projectPath}"/>
</exec>
</target>
Kajona verwendet PHPDocs zur Dokumentation des Quellcodes. Draus lässt sich automatisch eine hübsche, lesbare HTML-Ansicht generieren.
<!-- requires phpDocumentor, binary phpdoc -->
<target name="generatePhpDocs">
<echo>Generating PHP API Docs</echo>
<exec executable="phpdoc" dir="${basedir}${projectPath}">
<arg line=" -ue on -t '${basedir}${buildPath}/api'
-i *tcpdf*,*pchart*,*yui*,*ezcomponents*
-ti 'Kajona API Docs'
-dn modul_system
-d ."/>
</exec>
</target>
Die PHPUnit Tests sind dsa Herzstück des Build-Laufes. Nur wenn diese erfolgreich sind, dann wird der Build-Lauf fortgesetzt.
<!-- requires phpUnit, binary phpunit -->
<target name="runPhpUnitTests">
<echo>Running PHPUnitTests</echo>
<exec executable="phpunit" dir="${basedir}${projectPath}" failonerror="on">
<arg line="--log-junit '${basedir}${buildPath}/logs/junit.xml'
--coverage-clover '${basedir}${buildPath}/logs/clover.xml'
--coverage-html '${basedir}${buildPath}/coverage'
tests" />
</exec>
</target>
Der PHPCodeSniffer untersucht den Quellcode nach statischen / syntaktischen Fehlern. Ausgeschlossen werden hierbei externe Komponenten.
<!-- requires php PEAR code sniffer -->
<target name="runPhpCodeSniffer" >
<echo>Running PHP Code Sniffer</echo>
<exec executable="phpcs" dir="${basedir}${projectPath}" output="${basedir}${buildPath}/logs/checkstyle.xml">
<arg line="--report=checkstyle --tab-width=4 --standard=PEAR --ignore=*yui*,*tcpdf*,*fonts*,*.js,*pchart*,*ezcomponents* ."/>
</exec>
</target>
Der PHPMessDetector versucht u.A. Komplexitäten im Quellcode zu erkennen.
<!-- requires php PEAR Mess detector being available -->
<target name="runPhpMD" >
<echo>Running PHP Mess Detector</echo><!--output="${basedir}${buildPath}/logs/pmd.xml"-->
<exec executable="phpmd" dir="${basedir}${projectPath}" >
<arg line=". xml codesize,unusedcode,naming --exclude *yui*,*tcpdf*,*fonts*,*pchart* --reportfile '${basedir}${buildPath}/logs/pmd.xml' "/>
</exec>
</target>
Der PHPCopyPasteDetector versucht u.A. copy & paste Codeblöcke zu erkennen.
<!-- requires php PEAR CopyPaste detector being available -->
<target name="runPhpCPD" >
<echo>Running PHP Copy Paste Detector</echo>
<exec executable="phpcpd" dir="${basedir}${projectPath}">
<arg line="--log-pmd '${basedir}${buildPath}/logs/pmd-cpd.xml' . "/>
</exec>
</target>
Der PHPDependency Detector hilft beim analysieren der Komplexitäten von einzelnen Modulen.
<!-- requires php PEAR pdepend being available -->
<target name="runPDepend">
<echo>Running PHP Dependency Detector</echo>
<exec executable="pdepend" dir="${basedir}${projectPath}">
<arg line="--jdepend-xml='${basedir}${buildPath}/logs/jdepend.xml'
--phpunit-xml='${basedir}${buildPath}/logs/pdepend.xml'
--jdepend-chart='${basedir}${buildPath}/graph/10-dependencies.svg'
--summary-xml='${basedir}${buildPath}/logs/pdepend.xml'
--overview-pyramid='${basedir}${buildPath}/graph/11-software-metrics-pyramid.svg'
. "/>
</exec>
</target>
</project>
Nach der Anlage des Projektes muss CruiseControl mit den entsprechenden Eigenschaften des Projektes vertraut gemacht werden. Hierfür wird ein project-Eintrag in der Datei config.xml vorgenommen.
<cruisecontrol>
<system>
<configuration>
<threads count="2" />
</configuration>
</system>
<project name="kajona" buildafterfailed="false">
<plugin name="svn" classname="net.sourceforge.cruisecontrol.sourcecontrols.SVN" />
Alle 120 Sekunden soll CruiseControl nach Änderungen der Codebasis suchen. Erst wenn der letzte Checkin 60 Sekunden vergangen ist wird der Build-Lauf begonnen. Das vermeidet das Bauen in einer Kette von an sich zusammengehörenden Checkins.
<modificationset quietperiod="60">
<svn localWorkingCopy="projects/${project.name}/sources/" username="guest" password="guest" />
</modificationset>
<schedule interval="120">
<ant anthome="apache-ant-1.7.0" buildfile="projects/${project.name}/build.xml"/>
</schedule>
Definiert das Verzeichnis, in dem Logfiles abgelegt werden.
<log dir="logs/${project.name}">
<merge dir="projects/${project.name}/build/logs/"/>
</log>
Der Status-Listener wird v.A. für PhpUnderControl benötigt.
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
<publishers>
Die von dre build.xml gebauten Pakete sollen publiziert werden.
<!-- packages -->
<artifactspublisher dir="projects/${project.name}/packages"
dest="artifacts/${project.name}" subdirectory="packages"/>
Ebenso die erzeugten Coverage-Reports der PHPUnitTests
<!-- coverage report -->
<artifactspublisher dir="projects/${project.name}/build/coverage"
dest="artifacts/${project.name}" subdirectory="coverage"/>
Und natürlich die erzeugten PHP-Api-Docs
<artifactspublisher dir="projects/${project.name}/build/api"
dest="artifacts/${project.name}" subdirectory="api" />
Die hübschen Grafiken des Testlaufs bitte ebenso :).
<artifactspublisher dir="projects/${project.name}/build/graph"
dest="artifacts/${project.name}" subdirectory="graph" />
PhpUnderControl sollte nun die Möglichkeit bekommen, die Logfiles selbst zu analysieren und daraus Grafiken zu bauen.
<execute command="/opt/cruisecontrol_kajona/phpUnderControl/bin/phpuc.php graph logs/${project.name} artifacts/${project.name}"/>
Wenn gewünscht kann nun noch der zu verwendende Mailserver samt Alias-Mapping (SVN-Account <-> Mail-Account) definiert werden.
<email mailhost="mail.xxx.de"
username="aaaa"
password="bbb"
returnaddress="buildserver@xxx.de"
buildresultsurl="http://xxx/cruisecontrol/buildresults/${project.name}"
skipusers="false"
spamwhilebroken="true">
<always address="build@xxx.de"/>
<failure address="build-error@xxx.de"/>
<map alias="abc" address="a.bc@xxx.de" />
<map alias="cde" address="c.de@xyz.de" />
</email>
</publishers>
</project>
</cruisecontrol>