Released in March, Atalasoft's toolkit v6.0 had a bug release v6.0a and
we've branched to begin building new functionality for our new release while
being able to maintain/bugfix v6.0. In addition, we're internally
branched to have one codebase as a sandbox, of sorts. To a build
engineer, this may mean lots of work! First, you have to branch (which on
our codebase takes a long time in itself) not once, but twice. Each time
you branch, the engineers need to be able to build, which means all those pesky
files with version numbers need updating (check out the AtalaFileRegex
posts). But mostly, it means new integration targets and projects!
Seeing this coming, I wanted to breakout redundancy and use build files like
objects, and build targets like object methods.
The first step to this is making the environment work like it does if you
open the Visual Studio command prompt. This command prompt is really just
a regular console with one of a handful of batch files called
automatically. Depending on the hardware and version of VS, mostly its
responsibility is setting the PATH, LIBPATH and framework environment
variables. Once this is set, you can call all the VS tools without
thought. It'd be nice to have that setup before any NAnt script is called
so that any engineer can write a build task knowing they'll get the right
environment for the job. Let's have a look at how this is done.
Realizing that one would want to include this in all NAnt projects that
require a good environment setup, we'll call this file environment.include instead
of environment.build since it doesn't really build anything, anyway.
Next, a nifty trick most probably know about, but it's worth
mentioning. NAnt has built-in properties that you can edit, like the
nant.onsuccess and nant.onfailure properties, which take as a value the name of
a target to run in the appropriate case. They're kind of like destructors
in that they'll be called before leaving your script no matter what.
<!-- Redirect our nant events -->
<property name="nant.onsuccess" value="OnSuccess" />
<property name="nant.onfailure" value="OnFailure" />
What you do with those 'events' is up to you. We simple use them to
output some extra information to make logs easier to read:
<target name="OnSuccess">
<echo message="${datetime::now()}, --- BUILD FINISHED! ---" file="${Progress.Log.File}" append="true" failonerror="false"/>
</target>
<target name="OnFailure">
<echo message="${datetime::now()}, ****************** BUILD FAILED
********************" file="${Progress.Log.File}" append="true" failonerror="false"/>
</target>
Our environment script needs a bootstrap of sorts. The Visual Studio
command prompt is hard coded with the path to its installation directory, but I
don't want to hardcode that into the script. Instead, we'll let the user
tell us where their VS install dir is with an environment variable and begin
following the Visual Studio batch file. That way this script can be moved
around without editing:
<fail message="ERROR: The environment variable 'VSINSTALLDIR' was not found.
Please ensure Visual Studio 2008 is properly installed on the build machine." if="${not
environment::variable-exists('VSINSTALLDIR')}" />
<property name="VSINSTALLDIR" value="${environment::get-variable('VSINSTALLDIR')}" />
<property name="VCINSTALLDIR" value="${VSINSTALLDIR}\VC" />
Here, we tell the user that they must have the VSINSTALLDIR variable
set. Instead of using the default set VSxxCOMNTOOLS, we want to be a bit
more flexible and not hardcode a particular version, either.
In Atalasoft, we build our binaries for a few different .NET
frameworks. It's expected when the environment.include file is included
that a particular property, 'Framework.Version,' be set. To ensure that,
we'll let the user know with a failure message:
<fail message="ERROR: Unknown target framework, select either 2.0 or 3.5" if="${((Framework.Version
!= '2.0') and (Framework.Version != '3.5'))}" />
and ensure that the set framework is available to work with:
<readregistry property="FrameworkDir" key="SOFTWARE\Microsoft\.NETFramework\InstallRoot" hive="LocalMachine" if="${Framework.Version
== '2.0'}" failonerror="true" />
<property name="FrameworkDir" value="${directory::get-parent-directory(FrameworkDir)}\Framework64" if="${Atala64Bit}" />
<property name="FrameworkDir" value="${directory::get-parent-directory(FrameworkDir)}\Framework" if="${not Atala64Bit}" />
<property name="FrameworkVersion" value="v2.0.50727" if="${Framework.Version
== '2.0'}" />
<property name="FrameworkVersion" value="v3.5" if="${Framework.Version
== '3.5'}" />
<fail message="ERROR: The FrameworkDir, ${FrameworkDir}, does not exist on
this machine" if="${not directory::exists(FrameworkDir)}" />
Notice, we also use another property, 'Atala64Bit' which is also set by the
including script. This flag makes building easier later on, since we use
that to trigger which build configuration we use.
Finally, we get ready to set some environment variables, as they are in the
vcvars batch file:
<readregistry property="WindowsSDKDir" key="SOFTWARE\Microsoft\Microsoft
SDKs\Windows\CurrentInstallFolder" hive="LocalMachine" failonerror="false" />
<property name="DevEnv.Path" value="${environment::get-variable('VSINSTALLDIR')}" if="${environment::variable-exists('VSINSTALLDIR')}" />
<property name="DevEnv.Path" value="${DevEnv.Path}\Common7\IDE" />
<setenv verbose="true">
<variable name="FrameworkDir" value="${FrameworkDir}" />
<variable name="FrameworkVersion" value="${FrameworkVersion}"
/>
<variable name="Framework35Version" value="v3.5" />
<variable name="VCINSTALLDIR" value="${VCINSTALLDIR}" />
<variable name="WindowsSdkDir" value="${WindowsSDKDir}" if="${property::exists('WindowsSDKDir')}"/>
<variable name="PATH" value="${WindowsSDKDir}bin;${environment::get-variable('PATH')}" />
<variable name="INCLUDE" value="${WindowsSDKDir}include;${environment::get-variable('INCLUDE')}" />
<variable name="LIB" value="${WindowsSDKDir}lib;${environment::get-variable('LIB')}" />
<variable name="DevEnvDir" value="${DevEnv.Path}" />
</setenv>
I ran into a problem with %LIBPATH%, and found a way around the environment
script from halting when this fails. Perhaps there's a better way of
doing this, but this is good enough for now. We start by getting the
current settings from the environment, and saving them into properties:
<!-- This is where the environment
splits between 32/64-bit machines-->
<property name="VS90COMNTOOLS" value="${environment::get-variable('VS90COMNTOOLS')}" />
<property name="INCLUDE" value="${environment::get-variable('INCLUDE')}" />
<property name="PATH" value="${environment::get-variable('PATH')}" />
<property name="LIB" value="${environment::get-variable('LIB')}" />
<!-- We haven't set %LIBPATH% yet, so
may be empty. This circumvents that issue-->
<property name="LIBPATH" value="${environment::get-variable('LIBPATH')}" if="${environment::variable-exists('LIBPATH')}" />
<property name="LIBPATH" value=""
if="${not property::exists('LIBPATH')}" />
and then, as the vcvars batch file does, we tack on the settings we've discovered:
<setenv verbose="true">
<variable name="PATH" value="${DevEnv.Path};${VCINSTALLDIR}\BIN;${VS90COMNTOOLS};${FrameworkDir}\v3.5;${FrameworkDir}\${FrameworkVersion};${VCINSTALLDIR}\VCPackages;${PATH}"/>
<variable name="INCLUDE" value="${VCINSTALLDIR}\ATLMFC\INCLUDE;${VCINSTALLDIR}\INCLUDE;${INCLUDE}"/>
<variable name="LIB" value="${VCINSTALLDIR}\ATLMFC\LIB;${VCINSTALLDIR}\LIB;${LIB}"/>
<variable name="LIBPATH" value="${FrameworkDir}\v3.5;${FrameworkDir}\${FrameworkVersion};${VCINSTALLDIR}\ATLMFC\LIB;${VCINSTALLDIR}\LIB;${LIBPATH}" />
</setenv>
(Keep in mind, these are all going to be different if you're running
64-bit; I've chosen not to paste the whole file for brevity).
Finally, as an example of what we do at Atalasoft, we set the Build.Suffix
property. This is a convenience thing I use later on in the build script,
however, it shows that we have a few different build configurations to maintain
depending on the target:
<property name="Build.Suffix" value="_v8" if="${not Atala64Bit}"/>
<property name="Build.Suffix" value="_x64" if="${Atala64Bit}" />
<if test="${Framework.Version
== '2.0'}" >
<property name="nant.settings.currentframework" value="net-2.0" />
<property name="Build.Config"
value="AutomatedRelease_dotnet20" if="${not Atala64Bit}" />
<property name="Build.Config"
value="AutomatedRelease_dotnet20_64bit" if="${Atala64Bit}" />
<property name="Build.Description"
value=".NET 2.0"
if="${not Atala64Bit}"
/>
<property name="Build.Description"
value=".NET 2.0, 64-bit"
if="${Atala64Bit}"
/>
</if>
<if test="${Framework.Version
== '3.5'}" >
<property name="nant.settings.currentframework" value="net-3.5" />
<property name="Build.Config"
value="AutomatedRelease_dotnet35" if="${not Atala64Bit}" />
<property name="Build.Config"
value="AutomatedRelease_dotnet35_64bit" if="${Atala64Bit}" />
<property name="Build.Description"
value=".NET 3.5"
if="${not Atala64Bit}"
/>
<property name="Build.Description"
value=".NET 3.5, 64-bit"
if="${Atala64Bit}"
/>
</if>
That's pretty much it! A vcvarsall.bat file in NAnt language.
From here you can imagine a nice clean call to devenv.exe, without the need for
discovering the path. Best of all, put this in a central location on your
build server, and include it in all your build scripts like this:
<property name="Framework.Version" value="2.0" />
<property name="Atala64Bit" value="false" />
<include buildfile="${Dir.Build}\environment.include" failonerror="true" />
and your calls to devenv are simple, like this:
<exec program="devenv.com" commandline="/UseEnv /${Build.Type} ${Build.Config} "${Dir.Source}\SomeSolution.sln" /out ${Build.Log}" verbose="true" failonerror="true" />
I hope that streamlines some build processes out there. If anyone has
suggestions on how I can make this better, please let me know!