Welcome to Atalasoft Community Sign in | Help

A Visual Studio command prompt environment in NAnt

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 name="OnFailure">

<echo message="${datetime::now()}, ****************** BUILD FAILED ********************" file="${Progress.Log.File}" append="true" failonerror="false"/>


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}" />


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="LIB" value="${VCINSTALLDIR}\ATLMFC\LIB;${VCINSTALLDIR}\LIB;${LIB}"/>

<variable name="LIBPATH" value="${FrameworkDir}\v3.5;${FrameworkDir}\${FrameworkVersion};${VCINSTALLDIR}\ATLMFC\LIB;${VCINSTALLDIR}\LIB;${LIBPATH}" />


(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 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}" />


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} &quot;${Dir.Source}\SomeSolution.sln&quot; /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!


Published Wednesday, May 14, 2008 9:46 AM by dterrell
Filed under: ,


Thursday, May 15, 2008 12:45 PM by DotNetKicks.com

# A Visual Studio command prompt environment in NAnt

You've been kicked (a good thing) - Trackback from DotNetKicks.com

Anonymous comments are disabled