Some Batch File Wizardry for Automated Builds

At work and also on my private computer I am maintaining multiple Delphi projects that are (currently) managed in subversion repositories. They have a general structure like this:

projectname
  \- buildtools (-> svn:external)
  \- src (project sources)
  \- libs
    \- dzlib (-> svn:external)
    \- some other libraries, all svn:external

Being a lazy bastard (deutsch: faule Socke) I don’t want to start Delphi just to compile the projects or check whether they still compile but have a build script to do that. Also, I don’t want to right click and svn update all the time (and build after that) so I have got an autobuildcheck script. And last but not least, since on my computer all Delphi versions since version 6 are installed, I need a method to automatically launch the correct IDE for each of the projects.

First, auto_build_check.cmd:

That one is simple. It needs to

  • svn update
  • call the build script
  • revert the build number increment which the build script automatically made
  • if no error occurred, just exit, otherwise pause to let me examine the error message
rem Update sources from repository
rem Build them
rem in case of error(s): Stop and show the error
rem revert the automatic build number incrementation

svn update
if errorlevel 1 goto Error

set BatchBuild=1
call _BuildProject.cmd

if not exist src\*_version.ini goto :eof
svn revert src\*_version.ini
if errorlevel 1 goto error

goto :eof

:error
echo ***** ERROR *****
pause

Next, _buildproject.cmd:

It needs to

  • determine the project name from the current directory (I don’t want to write different build script for each project, lazy bastard again)
  • determine the Delphi version to use for building (this one only supports Delphi 2007 and later)
  • call msbuild
  • if an error occurs, show that error
  • pause, to let me examine the results
rem @echo off
REM - assumes that the parent directory is also the project name, extracts it
REM - and calls msbuild with %project%.dproj

setlocal

rem Has the project been passed as parameter?
set project=%1
if not "%project%"=="" goto projgiven
rem Is there a __SetProjectName.cmd which supposedly sets %project%?
if exist __SetProjectName.cmd call __SetProjectName.cmd
if not "%project%"=="" goto projgiven
rem Still no project, use the parent directory name to get it
call :GetLastDir %0
set project=%result%

:projgiven

call buildtools\delphiversions.cmd

set DelphiVersion=Delphi 2007
set DelphiDir=%Delphi2007Dir%
if exist __SetDelphiVersion.cmd call __SetDelphiVersion.cmd

set OldPath=%PATH%
echo building project %project%.dproj using %DelphiVersion%
call "%DelphiDir%\bin\rsvars.bat"

if NOT "%DelphiVersion%"=="Delphi 2007" goto no2007
rem Delphi 2007 sets the FrameworkDir incorrectly on 64 bit Windows, so we fix this here
SET FrameworkDir=%SystemRoot%\Microsoft.NET\Framework\
SET PATH=%FrameworkDir%%FrameworkVersion%;%FrameworkSDKDir%;%OldPath%
:no2007

pushd src
msbuild %project%.dproj | ..\buildtools\msbuildfilter ..\errors.txt
popd
if errorlevel 1 goto error

endlocal
if "%BatchBuild%"=="1" goto nopause
pause
:nopause
goto :eof

:error
echo ************************************
echo ***** Error building %project% *****
echo ************************************
if "%MAILSend%"=="" goto nomail
%MAILSend% -sub "Error building %project%" -M "Build Error" -attach errors.txt,text/plain,a
endlocal
goto :eof

:nomail
endlocal
pause
goto :eof

:GetLastDir
rem extract path
set directory=%~p1%
rem remove backslash (=last character)
set directory=%directory:~0,-1%
rem extract "filename" (= last directory of path)
call :LastItem %directory%
goto :eof

:LastItem
set result=%~n1%
goto :eof

Actually, this script does some more things:

  • It allows me to pass the project name as a parameter.
  • If no parameter is passed, it looks for a file called __SetProjectName in the current directory, which can set %project%.
  • If that doesn’t exist either, it calls the GetLastDir subroutine to get the last part of the current directory and uses that as the project.
  • next, it calls buildtools\delphiversions.cmd which is a script that knows where all my Delphi versions are installed and sets the base directories for them as Delphi2007Dir, Delphi2009Dir etc. environment variables.
  • Determine wichh Delphi version it should use. It assumes Delphi 2007 (which I still use for the majority of my projects), looks for __SetDelphiVersion.cmd in the current directory, which is supposed to set %DelphiVersion% and %DelphiDir% if a different version is to be used.
  • If Delphi 2007 is to be used, it implements a workaround to fix a problem this version has with setting %FrameworkDir% on 64 bit Windows.
  • It calls rsvars.bat (which is a script installed by Delphi)
  • It switches to the src subdir
  • It calls msbuild for %project%.dproj (msbuildfilter is an internal tool that filters the output and counts hints, warnings and errors)
  • If there is no error it either pauses, or if in batchbuild mode, it just exists.
  • If there is an error, it checks whether it should send an e-mail and does that (I’ll come back to that in a future blog post.).
  • If it sent an e-mail, it exists, otherwise it pauses to let me examine the error.

Last, _OpenInIde.cmd

It needs to:

  • Determine the project name (see above)
  • Determine the Delphi version to use (again: see above)
  • Call the IDE of that version and pass it the appropriate .dproj file
@echo off
REM - assumes that the parent directory is also the project name, extracts it
REM - and calls the IDE with %project%.dproj

setlocal

rem Has the project been passed as parameter?
set project=%1
if not "%project%"=="" goto projgiven
rem Is there a __SetProjectName.cmd which supposedly sets %project%?
if exist __SetProjectName.cmd call __SetProjectName.cmd
if not "%project%"=="" goto projgiven
rem Still no project, use the parent directory name to get it
call :GetLastDir %0
set project=%result%

:projgiven

call buildtools\delphiversions.cmd
set DelphiVersion=Delphi 2007
set DelphiDir=%Delphi2007Dir%
if exist __SetDelphiVersion.cmd call __SetDelphiVersion.cmd

start "%DelphiVerson%" "%DelphiDir%\bin\bds.exe" -pDelphi src\%project%.dproj
endlocal
goto :eof

:GetLastDir
rem extract path
set directory=%~p1%
rem remove backslash (=last character)
set directory=%directory:~0,-1%
rem extract "filename" (= last directory of path)
call :LastItem %directory%
goto :eof

:LastItem
set result=%~n1%
goto :eof

So, am I lazy? I don’t really know. Writing the scripts described above was quite a lot of work. On the other hand, that was interesting work and it keeps me from making stupid errors again and again, like opening a project with the wrong Delphi version and having to revert the changes done by that version, or forgetting to do svn update before checking if the project still compiles. Also, they allow me to run a continuous build server to automatically find compile errors caused by incompatible changes to one of the libraries used, before I or my coworkers forget the reason for that change.

But why have copies of these scripts in all my projects rather than having them in buildtools and only small stubs in the project, that call these scripts? So that’s the next step.

If you want to use these scripts in your own projects or roll your own based on them, feel free to do so. You can find them in my buildtools repository.