Search This Blog

Loading...
Showing posts with label NAnt. Show all posts
Showing posts with label NAnt. Show all posts

Wednesday, November 05, 2008

SQL Server Scheduled Backups with NAnt

When installing and using production web applications, backup is always a must. I have written about SQL Server Backup before and this post would expend on that. The main goal here is to create a solution that periodically backs up your database. To do this, we’ll use the backup script from the previous post, employ NAnt to execute the script and zip the created backup. Last, we’ll schedule the execution of the NAnt build script through the Windows Task Scheduler or the “at” command. This procedure can backup a local or a remote SQL Server (as long as the remote server is on the same network).

Prerequisites

  • Installed and configured version of NAnt (see Getting Started with NAnt - .NET Build Tool)
  • Installed NAntContrib tasks (for the SQL task, see NAntContrib on SourceForge)
  • Task Scheduler Service enabled in Control Panel/Admin Tools/Services
  • SQL login with “dbo” rights to the database (to install the backupDatabase procedure)
  • SQL login that has “public” rights to the database (so it can execute the backup)

Backup SQL Procedure

The script below with create the SQL server stored procedure to create a backup file of a given database. It takes two parameters:

  1. databaseName – The SQL server database to be backed up
  2. backupDirectory – The directory where the backup file will be created

You need to execute it against the database that you will be backing up and give the “public” role execution permissions to the procedure.


Code
exec dbo.sp_executesql @statement = N'
/*
Created:
 07.18.2008 by Boyan Kostadinov (boyank@gmail.com)

Dependencies:
 None

Usage:
 exec dbo.backupDatabase ''ensembleVideo'', ''C:\Temp''

Parameters:
 @databaseName - varchar
 - The database to backup

 @backupDirectory - varchar
 - The path to where the database should be
 backed up. This should be an existing directory on
 the SQL Server where the database is located

Description:
 Backsup a given database to the specified directory
*/
create procedure dbo.backupDatabase
	@databaseName varchar(100),
	@backupDirectory varchar(1000)
as
declare @backupFileName varchar(100),
	@databaseDataFilename varchar(100), @databaseLogFilename varchar(100),
	@databaseDataFile varchar(100), @databaseLogFile varchar(100),
	@execSql varchar(1000)

-- If the backup directory does not end with ''\'', append one
if charindex(''\'', reverse(@backupDirectory)) > 1
	set @backupDirectory = @backupDirectory + ''\''

-- Create the backup file name based on the backup directory, the database name and today''s date
set @backupFileName = @backupDirectory + @databaseName + ''-'' + replace(convert(varchar, getdate(), 110), ''-'', ''.'') + ''.bak''

set @execSql = ''
backup database ['' + @databaseName + '']
to disk = '''''' + @backupFileName + ''''''
with
  noformat,
  noinit,
  name = '''''' + @databaseName + '' backup'''',
  norewind,
  nounload,
  skip''

exec(@execSql)'
go

NAnt Backup Script

The script is somewhat complex. Here are the list of features:

  • Backup a local SQL Server instance or a SQL Server instance on the same network. The key is that the file system of the networked instance must be available through UNC shares to the machine executing the backup script.
  • Use a specified connection string or read the connection string from a .NET (or other type of XML)configuration file.

I will not dive into the script itself since it's properties and flow is documented fairly well.

Code

<project name="backupDatabase" default="run" xmlns="http://nant.sf.net/release/0.86-beta1/nant.xsd">
	<!-- Set the name of the backup file that will be created -->
	<property name="zipFileName" value="databaseBackup" overwrite="false" />

	<!-- Set the path where the backup file will be finally stored -->
	<property name="localBackupDirectory" value="C:\Temp" overwrite="false" />

	<!-- Set the local path (relative to the SQL Server instance) where SQL Server will write the backup file -->
	<!-- This has to always be a local path since SQL server can't write to network paths -->
	<property name="sqlServerLocalBackupDirectory" value="C:\Temp" overwrite="false" />

	<!-- Set the UNC path to the above "sqlServerLocalBackupDirectory" local path -->
	<!-- This is only needed if you are backing up SQL server on the network -->
	<property name="sqlServerUNCBackupDirectory" value="\\beehive\windows$\Temp" overwrite="false" />
	<!-- For backing up a local SQL Server instance, commented the first "sqlServerUNCBackupDirectory" property -->
	<!-- and uncommented the one below this line, or set the "sqlServerUNCBackupDirectory" property to "" -->
	<!--<property name="sqlServerUNCBackupDirectory" value="" overwrite="false" />-->

	<!-- Set the .NET connection string for connecting to the database -->
	<!-- This setting always takes precedence over the config file below -->
	<!-- To use a config file instead, set this property to "" (like so value="") -->
	<property name="connectionString" value="Data Source=beehive\sql2005;Initial Catalog=ensembleVideo;User ID=test;Password=test;" overwrite="false" />

	<!-- Alternatively set the path to where the build script should get the connection string from  -->
	<!-- This is usually a app.config or connectionString.config file -->
	<property name="connectionStringConfigFilePath" value="C:\Inetpub\wwwroot\myApp\config\connectionStrings.config" overwrite="false" />

	<!-- Set the XPath expression that will be used to grab the connection string from the config file -->
	<property name="connectionStringXPath" value="/connectionStrings/add[@name = 'sqlServerConnection']/@connectionString" overwrite="false" />

	<!-- Set the regular expression that's needed to get the database name from the connection string -->
	<property name="getDatabaseNameFromConnecionStringRegEx" value="Initial Catalog=(?'databaseName'.*?);" overwrite="false" />

	<property name="todaysDate" value="${string::substring(string::replace(datetime::to-string(datetime::now()), '/', '.'), 0, 10)}" />
	<property name="todaysLocalBackupDirectory" value="${path::combine(localBackupDirectory, todaysDate)}" />

  	<target name="run">
		<!-- If the SQL Server UNC directory was not specified and the local backup directory exists -->
		<if test="${string::get-length(sqlServerUNCBackupDirectory) == 0 and directory::exists(localBackupDirectory)}">
			<!-- This is a back of a local SQL Server instance -->
			<mkdir dir="${todaysLocalBackupDirectory}" />

			<property name="todaysSqlServerLocalBackupDirectory" value="${todaysLocalBackupDirectory}" />
			<property name="todaysSqlServerUNCBackupDirectory" value="${todaysLocalBackupDirectory}" />

			<property name="localSqlServer" value="true" />
		</if>

		<if test="${string::get-length(sqlServerUNCBackupDirectory) > 0 and directory::exists(sqlServerUNCBackupDirectory) }" >
			<property name="todaysSqlServerLocalBackupDirectory" value="${path::combine(sqlServerLocalBackupDirectory, todaysDate)}" />
			<property name="todaysSqlServerUNCBackupDirectory" value="${path::combine(sqlServerUNCBackupDirectory, todaysDate)}" />

			<!-- This is a back of a networked SQL Server instance -->
			<mkdir dir="${todaysSqlServerUNCBackupDirectory}" />

			<property name="localSqlServer" value="false" />
		</if>

		<!-- If the connection string is empty,
		the connection string file exists and the XPath to find the connection string is not empty -->
		<if test="${string::get-length(connectionString) == 0 and file::exists(connectionStringConfigFilePath)
							and string::get-length(connectionStringXPath) > 0}" >
			<!-- Get the connection string to the database from the connection string config file -->
			<xmlpeek
				file="${connectionStringConfigFilePath}"
				xpath="${connectionStringXPath}"
				property="connectionString">
			</xmlpeek>
		</if>

		<!-- If the connectionString property is not empty and the regular expression to get the database name is not empty -->
		<if test="${string::get-length(connectionString) > 0 and string::get-length(getDatabaseNameFromConnecionStringRegEx) > 0}">
			<!-- Get the database name from the connection string -->
			<regex pattern="${getDatabaseNameFromConnecionStringRegEx}" input="${connectionString}" />
	
	    		<!-- Execute the stored procedure to bckup the database -->
			<sql connstring="Provider=SQLOLEDB;${connectionString}" transaction="false" delimiter=";" delimstyle="Normal">
			exec dbo.backupDatabase '${databaseName}', '${todaysSqlServerLocalBackupDirectory}';
			</sql>

			<!-- Zip up the created databse backup file -->
			<zip zipfile="${path::combine(todaysSqlServerUNCBackupDirectory, zipFileName + '-' + todaysDate + '.zip')}" ziplevel="9">
				<fileset basedir="${todaysSqlServerUNCBackupDirectory}">
					<exclude name="**/*.zip" />
					<include name="*.*" />
				</fileset>
			</zip>

			<!-- Delete all other files in the today's backup directory except for the created zip files -->
			<delete>
				<fileset basedir="${todaysSqlServerUNCBackupDirectory}">
					<exclude name="**/*.zip" />
					<include name="*.*" />
				</fileset>
			</delete>

			<!-- Move the contents of today's backup directory to the local backup directory -->
			<move todir="${localBackupDirectory}">
				<fileset basedir="${todaysSqlServerUNCBackupDirectory}">
					<include name="*.*" />
				</fileset>
			</move>

			<!-- Delete the "todays" directories -->
			<if test="${localSqlServer}">
				<delete dir="${todaysLocalBackupDirectory}" />
			</if>

			<if test="${not localSqlServer}">
				<delete dir="${todaysSqlServerUNCBackupDirectory}" />
			</if>
		</if>
  </target>
</project>

Setting Up Backup Script and Scheduling

Review the build script and set following properties to match your setup:
  • zipFileName – The name of the zip file that will be created for the database backup
  • localBackupDirectory – The local directory where the backup will be stored
  • sqlServerLocalBackupDirectory - The local path (relative to the SQL Server instance) where SQL Server will write the backup file. This has to always be a local path since SQL server can't write to network paths.
  • sqlServerUNCBackupDirectory – The UNC path to the above "sqlServerLocalBackupDirectory" local path. This is only needed if you are backing up SQL server on the network. For backing up a local SQL Server instance, set this "”.
  • connectionString - The .NET connection string for connecting to the database. This setting always takes precedence over the “connectionStringConfigFilePath“ setting. To use a configuration file instead, set this property to "".
  • connectionStringConfigFilePath - Alternatively set the path to where the build script should get the connection string from. This is usually a app.config or connectionString.config file.
  • connectionStringXPath - The XPath expression that will be used to grab the connection string from the configuration file.
  • getDatabaseNameFromConnecionStringRegEx - The regular expression that's needed to get the database name from the connection string.


The next step is to create the schedule with either the Task Scheduler or with “at” command.

To use the Task Scheduler:

  1. Create a .bat file with the following: “driveLetter:\path\to\nant.exe /f:pathToNAntBackupScript.build” and of course replace that with the actual path to nant.exe and to the NAnt build script you got here.
  2. Go to Control Panel / Scheduled Tasks / Add Scheduled Task
  3. Browse for the .bat file you created in #1
  4. Configure the schedule run as often as you would like

To use the “at” command:

  1. Do the same as #1 above.
  2. Open a command prompt and execute the “at” command:
    ”at 23:00 /every:M,T,W,Th,F pathToBatFileFromStep1.bat”

    That will schedule the task to execute every day of the week at 11:00pm. You can get more info on the “at” command from How To Use the AT Command to Schedule Tasks.

Bonus

You don’t have to hard code the values in the NAnt build script. You can pass them from the command line. In that manner you can reuse the same script for different database. You simply need to call the script with –D:propertyName=”value" like so:

path\to\nant.exe /f:pathToNAntBackupScript.build –D:zipFileName="myDatabase" –D:localBackupDirectory="D:\Temp"

Downloads

http://tech-cats.net/blog/downloads/sql/procedure-dbo.backupDatabase.txt
http://tech-cats.net/blog/nantScripts/backupDatabase.build
http://tech-cats.net/blog/nantScripts/backupDatabase.bat

Monday, November 03, 2008

Using NAnt to Traverse Directories and Execute SQL

In the latest version of Ensemble Video I had a need to migrate the old mechanism of showing content to new way that involved a single page and some URL rewriting (more on that another time). The basics of the old mechanism were that for each publishing point used by the application, a separate directory with specific “index.aspx” was created. That was always a major pain when updates had to be applied since I always had to preserve the old files but still update existing content. Long story short, I got away from that but I still needed to figure out what publishing points were in use so I can set the database flag for the new mechanism. So, the NAnt fanatic that I am, I build a NAnt script to do that. In this example, you will learn:

  1. How to parse XML for a certain value
  2. How to traverse directories with NAnt
  3. How to execute an “if” statement in NAnt
  4. How to get file and directory names using the NAnt built-in functions
  5. How to execute SQL script with SQL task from NAntContrib

The prerequisites are that 1). You have NAnt installed and 2). You have NAntContrib installed.

So let’s go one by one from the list above and you will see the final script at the end.

  1. How to parse XML for certain value
    <xmlpeek
    file="${path::combine(path::get-full-path(webApplicationDirectory), 'config\connectionStrings.config')}" xpath="/connectionStrings/add[@name = 'ensembleVideoConnection']/@connectionString" property="connectionString" />

    This parses the XML file under webApplicationDirectory\config\connectionStrings.config and uses an XPath expression to get the property of the connectionString with the name ensembleVideoConnection. The value is stored in the NAnt property "connectionString"
  2. How to traverse directories with NAnt
    <foreach item="Folder" in="${sitesDirectory}" property="orgDirectoryName">
    	<echo message="${orgDirectoryName}" />
    </foreach>

    This task traverses through the directories inside the directory specified by "sitesDirectory", stores the current directory in orgDirectoryName and then echoes the current directory.
  3. How to execute an “if” statement in NAnt
    <if test="${string::to-lower(path::get-file-name(orgDirectoryName)) != '_svn'}">
    	<echo message="${path::get-file-name(orgDirectoryName)}" />
    </if>

    Here we test if the current directory name is equal to "_svn". Here is another example where we test if the current directory contains a desired file:
    <if test="${file::exists(path::combine(webSiteDirectoryName, 'index.aspx'))}">
    	<property name="enableQuickPublish" value="1" />
    </if>

  4. How to get file and directory names using the NAnt built-in functions
    <-- Get only the directory name -->
    path::get-file-name(orgDirectoryName)
    <-- Get the directory name from the path -->
    path::get-directory-name(orgDirectoryName)
    <-- Create a full path based on the directory name and a file name -->
    path::combine(webSiteDirectoryName, 'index.aspx')

    You can find out more about NAnt built-in functions at NAnt Functions
  5. How to execute SQL script with SQL task from NAntContrib
    <sql
      connstring="Provider=SQLOLEDB;${connectionString}"
      transaction="true"
      delimiter=";"
      delimstyle="Normal">
    	update	dbo.webSites
    	set	isQuickPublished = ${enableQuickPublish}
    	where	webSiteID = (
    			select	webSiteID
    			from	dbo.listWebSites
    			where	webSafeOrganizationName = '${path::get-file-name(orgDirectoryName)}'
    				and webSafeDepartmentName = '${path::get-file-name(departmentDirectoryName)}'
    				and webSafeWebSiteName = '${path::get-file-name(webSiteDirectoryName)}'
    		);
    </sql>

    There is not much to the SQL statement execution. As you can see you can use NAnt properties and function inside the SQL script. You can check out all of NAntContrib's tasks at NAntContrib Tasks

As promised here is the full contents of the build script:

<project name="renameSqlToText" default="run" xmlns="http://nant.sf.net/release/0.86-beta1/nant.xsd">
	<!-- Set the path to the web application -->
	<property name="webApplicationDirectory" value="D:\_Boyan's Documents\_Projects\_ensembleVideo\trunk\webApplication\app" />

	<!-- Set the name of the sites directory (under the applicaiton directory) -->
	<property name="sitesDirectoryName" value="sites" />

	<property name="sitesDirectory" value="${path::combine(path::get-full-path(webApplicationDirectory), sitesDirectoryName)}" overwrite="true" />

	<target name="run">
		<!-- Get the database connection string from the web application -->
		<xmlpeek
			file="${path::combine(path::get-full-path(webApplicationDirectory), 'config\connectionStrings.config')}"
			xpath="/connectionStrings/add[@name = 'ensembleVideoConnection']/@connectionString"
			property="connectionString" />

		<!-- For each directory in the sitesDirectory -->
		<foreach item="Folder" in="${sitesDirectory}" property="orgDirectoryName">
			<if test="${string::to-lower(path::get-file-name(orgDirectoryName)) != '_svn'}">
				<echo message="${path::get-file-name(orgDirectoryName)}" />

				<foreach item="Folder" in="${orgDirectoryName}" property="departmentDirectoryName">
					<echo message="		${path::get-file-name(departmentDirectoryName)}" />

					<foreach item="Folder" in="${departmentDirectoryName}" property="webSiteDirectoryName">
						<echo message="				${path::get-file-name(webSiteDirectoryName)}" />

						<!-- Set the 'enableQuickPublish' property to false -->
						<property name="enableQuickPublish" value="0" />

						<!-- If a index.aspx file exists, set the 'enableQuickPublish' property to true -->
						<if test="${file::exists(path::combine(webSiteDirectoryName, 'index.aspx'))}">
							<property name="enableQuickPublish" value="1" />
						</if>

						<!-- Run a database query to update the flag in the database -->
					      	<sql
						        connstring="Provider=SQLOLEDB;${connectionString}"
						        transaction="true"
						        delimiter=";"
						        delimstyle="Normal">
							update	dbo.webSites
							set	isQuickPublished = ${enableQuickPublish}
							where	webSiteID = (
									select	webSiteID
									from		dbo.listWebSites
									where		webSafeOrganizationName = '${path::get-file-name(orgDirectoryName)}'
											and
											webSafeDepartmentName = '${path::get-file-name(departmentDirectoryName)}'
											and
											webSafeWebSiteName = '${path::get-file-name(webSiteDirectoryName)}'
									);
					      </sql>
					</foreach>
				</foreach>
			</if>
		</foreach>
	</target>
</project>

Downloads

http://tech-cats.net/blog/nantScripts/traverseDirectoriesAndExecuteSQL.build

Wednesday, July 23, 2008

Rename All Files in a Directory with NAnt

It’s a pretty simple thing to do but I had to spend a few minutes today to figure out how to accomplish it. So here is a quick build file to rename all .sql files in a specified directory (relative to the script run directory) to .txt. Funny enough, this scripts gets called by my automation program before it uploads any SQL scripts to my blog. There is nothing to it and there are comments so here goes:

<project name="renameSqlToText" default="run" xmlns="http://nant.sf.net/release/0.86-beta1/nant.xsd">
 <!-- The relative path to the directory where the files are located -->
 <property name="sqlDirectory" value="..\downloads\sql" />

 <target name="run">
  <!-- For each file in the directory -->
  <foreach item="File" in="${sqlDirectory}" property="fileName">
   <!-- If the file has the .sql extension -->
   <if test="${string::to-lower(path::get-extension(fileName)) == '.sql'}">
    <!-- Rename the file to .txt extension -->
    <move file="${fileName}" tofile="${path::combine(path::get-directory-name(fileName), path::get-file-name-without-extension(fileName) + '.txt')}" />
   </if>
  </foreach>
 </target>
</project>

Downloads

http://tech-cats.net/blog/nantScripts/renameSqlToText.build

Thursday, February 21, 2008

Tools for Authoring NAnt Build Files

NAnt is a great tool for automating many computer tasks. For a quick introduction, check my previous articles Automating Your Computer Tasks with NAnt and Use NAnt and WinRar to Create a Self Extracting Archive. Authoring NAnt build files can be difficult without a good editor and editing XML is not my favorite thing to do anyway. Having to know all the tags and parameters for each NAnt task, makes it even less desirable. Looking at the current options, you can find several tools for authoring NAnt build files:

While the tools above will do the job, it would be nice if you could edit your build files in Visual Studio and have at least the basic intellisense that the IDE provides. It turns out that editing NAnt build files with VS and having code insight is a pretty easy thing to setup. Here is how it is done:

  1. Install the NAnt schema by copying the file "nant.xsd" form the NAnt distribution to "C:\Program Files\Microsoft Visual Studio 8\Xml\Schemas". Update: for VS 2008, the directory is "C:\Program Files\Microsoft Visual Studio 9.0\Xml\Schemas"
  2. Associate NAnt build files (.build) with the Visual Studio XML editor. This can be done in one of two ways:
    • Create a registry merge file with the following contents and merge it in your registry:
      Windows Registry Editor Version 5.00
      
      [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\Editors\{412B8852-4F21-413B-9B47-0C9751D3EBFB}\Extensions]
      "build"=dword:00000029
      
    • Make the association through Visual Studio:
      • Open any Visual Studio solution
      • Add a NAnt build file to it
      • Right click on the .build file and choose "Open With"
      • Select "XML Editor" and click on "Set as Default"
  3. Almost there. The last thing is to add the "xmlns" (namespace) attribute to the "project" tag of your build file. If you have copied the NAnt schema file (nant.xsd) to the right place, adding the "xmlns" attribute should list the NAnt schema as one of the available choices. The end result should look like:
    <project
     name="testProject"
     default="buildSetup"
     basedir="."
     xmlns="http://nant.sf.net/release/0.85/nant.xsd"
    >
    
  4. And Voila! Start typing some XML and you should see a list of NAnt tasks and attributes.

Tuesday, January 29, 2008

Build Your SubSonic DAL with NAnt

SubSonic is a .NET ORM (object relational mapping) tool with plenty of extras. In the ORM market, it can be compared to other tool such as Linq and NHibernate. I chose SubSonic over NHibernate, because of the ease of configuration or Linq, because I am still programming for .NET 2.0. That being said, there are three different ways to generate your data access layer using SubSonic:

  1. Using the command line by calling "sonic.exe"
  2. Using the SubSonic Tools for Visual Studio
  3. Using a build provider. However, this method is only available to web projects.

I was using the second method by installing the tools for Visual Studio. However, that got to be a pain since every time I want to regenerate the data access layer I have to start Visual Studio, wait for it to load, invoke the SubSonic tools, wait for the generation and finally rebuild the project.

So a few days ago I switched to using the first method - the command line. Once I did that, it was a matter of time before I wrote the NAnt build file to do it for me without Visual Studio - generate the data access layer and rebuild the data access layer dll. So here is the build file that can be used stand alone or even invoked through a pre-build event in Visual Studio:

<project name="Ensemble" default="build" basedir=".">
  <!-- The full path to the SubSonic directory where SubSonic.dll is found -->
  <property name="subSonicFullPath" value="c:\Program Files\SubSonic\SubSonic 2.0.3" />

  <!-- The full path to the SubSonic commander -->
  <property name="sonicCommanderFullPath" value="C:\Program Files\SubSonic\SubSonic 2.0.3\SubCommander\sonic.exe" />

  <!-- The command line arguments for the SubSonic commander -->
  <property name="sonicCommanderArguments" value="generate /lang vb" />

  <!-- The root namespace for the project -->
  <property name="build.rootNamespace" value="ensembleVideo" />

  <!-- The build type (release or debug) for the project -->
  <property name="build.config" value="release" />

  <!-- The relative path to the project directory (from the location of the build file) -->
	<property name="targetDirectoryRelativePath" value="../dataAccessLayer" overwrite="false" />

	<!-- The relative path to directory where SubSonic will generated the files (from the project target directory) -->
	<property name="subsonicGeneratedFilesRelativePath" value="generated" />

  <property name="targetDirectoryFullPath" value="${path::get-full-path(targetDirectoryRelativePath)}" overwrite="false" />
  <property name="subsonicGeneratedFilesFullPath" value="${path::combine(targetDirectoryFullPath, 'generated')}" />
	<property name="binDirectory" value="${path::combine(targetDirectoryFullPath, 'bin')}" overwrite="false" />

  <property name="buildDirectoryFullPath" value="${path::combine(binDirectory, build.config)}" overwrite="false" />
  <property name="buildOutput" value="ensembleVideo.dataAccessLayer.dll" />

  <!-- Default build target -->
	<target name="build">
	  <!-- Execute the SubSonic commander to generated the files in the defined "sonicCommanderFullPath" directory -->
	  <exec
    	basedir="${targetDirectoryFullPath}"
    	program="${sonicCommanderFullPath}"
    	commandline="${sonicCommanderArguments} /out "${subsonicGeneratedFilesFullPath}""
    	workingdir="${targetDirectoryFullPath}"
    	failonerror="true" />

    <!-- Execute the vb compiler to compile the SubSonic generated files -->
    <vbc target="library" output="${path::combine(buildDirectoryFullPath, buildOutput)}" rootnamespace="${build.rootNamespace}">
        <imports>
            <import namespace="System" />
            <import namespace="System.Data" />
        </imports>

        <sources>
          <!-- Include all the SubSonic generated files -->
          <include name="${subsonicGeneratedFilesFullPath}\*.vb" />
        </sources>

        <!-- Include a reference to the SubSonic dll -->
        <references basedir="${subSonicFullPath}">
  			  <include name="SubSonic.dll" />
  		  </references>
     </vbc>
	</target>
</project>

Friday, January 04, 2008

NAnt, What Operating System is This?

I needed a quick way to tell what operating system the NAnt build file was running under. Why, one might wonder? Let's say you are installing a web application under IIS and you need the ASP.NET extension to be enabled. If you are wondering what that means, it's simple, with the introduction of IIS 6 (under Windows 2003), individual extensions (such as ASP or ASP.NET) can be enabled or disabled by the system administrator. So back to the why, you need to install a ASP.NET web application under IIS but to make things more complicated (as I often like to do), the ASP.NET extension might be disabled. So, you create a NAnt build file to do all the copy/setup steps for your web application installation (if you don't know what NAnt is, check out my other post titled Getting Started with NAnt - .NET Build Tool) but now you need to check if the ASP.NET extension is enabled. Before you can do that, you have to remember that only IIS 6 under Windows 2003 supports enabling/disabling extensions. If you try that under Windows XP (IIS 5) or Windows 2000 (IIS 5), your build file with barf. So to make a long story short, I need a test that will tell me if the NAnt build file is running under Windows 2003 or another operating system. So let's start with the NAnt build file that will output the current operating system:

<project name="testOS" default="runTest">
  <property name="operatingSystem" value="${operating-system::to-string(environment::get-operating-system())}" />

  <target name="runTest">
    <echo message="${operatingSystem}" />
  </target>
</project>

On Windows 2000 with Service Pack 4, that will output "Microsoft Windows NT 5.0.2195 Service Pack 4"
On Windows XP with Service Pack 2, that will output "Microsoft Windows NT 5.1.2600 Service Pack 2"
On Windows 2003 with Service Pack 2, that will output "Microsoft Windows NT 5.2.3790 Service Pack 2"
So now, to test if the NAnt build file is running under Windows 2003, we simply use "string::contains":
<if test="${string::contains(operatingSystem, 'Microsoft Windows NT 5.2')}">
  <echo message="This is Windows 2003" />
</if>

The complet NAnt build file to test the operating system should look like:
<project name="testOS" default="runTest" basedir=".">
  <property name="operatingSystem" value="${operating-system::to-string(environment::get-operating-system())}" />

  <target name="runTest">
    <if test="${string::contains(operatingSystem, 'Microsoft Windows NT 5.0')}">
      <echo message="This is Windows 2000" />
    </if>

    <if test="${string::contains(operatingSystem, 'Microsoft Windows NT 5.1')}">
      <echo message="This is Windows XP" />
    </if>

    <if test="${string::contains(operatingSystem, 'Microsoft Windows NT 5.2')}">
      <echo message="This is Windows 2003" />
    </if>
  </target>
</project>

Tuesday, November 27, 2007

NAnt Create SFX Archive Task

I have been working on a installer for a .NET web application with a database back-end. The installer itself deserves a series of posts since it was fairly complicated to get everything to work. An integral part of the installer was using NAnt for various things. The final stage in creating the ready to deploy executable is packaging the files in a self-extracting archive that extracts and runs the setup. With the help of Winrar and NAnt, that task can be automated. Let's get started. Requirements:

WinRar SFX options file sample:

; The path to the setup executable Setup=Setup.msi ; Extract the files to a temporary directory TempMode ; Use semi-silent mode Silent=2 ; Overwrite any existing files Overwrite=1 ; The title of the SFX archive Title=Sample Setup ; The text to show initially when the user clicks on the SFX archive (will only matter if using Silent=0) Text { The installer will extract and run the setup }

NAnt build file sample:

<project name="SampleSFX" default="createSFX" basedir=".">
 <description>createSFX task for creating a sample self-extracting installer</description>

 <!-- The name of the archive to be created -->
 <property name="archiveName" value="SampleSFX" overwrite="false" />

 <!-- The target directory from where the build will be invoked -->
 <property name="targetDirectory" value="." overwrite="false" />

 <!-- The relative path (from the targetDirectory) to directory containing the files to be archived -->
 <property name="archiveDirectory" value="<RelativePathToTheDirectoryToArchive>" overwrite="false">

 <!-- The mask of the file extensions to be archived -->
 <property name="archiveFileMask" value="*.*" overwrite="false" />

 <!-- The path to the winrar executable -->
 <property name="winrarPath" value="c:\program files\winrar\winrar.exe" overwrite="false" />

 <!-- The winrar command line switches for creating the archive -->
 <property name="winrarSwitches" value="a -ep -ep1 -r -sfxdefault.sfx" overwrite="false" />

 <!-- The name of the sfx options file to use while creating the archive -->
 <property name="sfxOptionsFile" value="sfxoptions.txt" overwrite="false" />

 <!-- The relative path (from the targetDirectory) to icon file to be used for the created archive -->
 <property name="sfxIconFile" value="images\setupIcon.ico" overwrite="false" />

 <property name="archiveDirectoryFullPath" value="${path::combine(path::get-full-path(targetDirectory), archiveDirectory)}" overwrite="true" />
 <property name="sfxOptionsFileFullPath" value="${path::combine(path::get-full-path(targetDirectory), sfxOptionsFile)}" overwrite="true" />
 <property name="sfxIconFileFullPath" value="${path::combine(path::get-full-path(targetDirectory), sfxIconFile)}" overwrite="true" />

 <target name="createSFX" description="Creates the self-extracing installer archive">
  <exec
  basedir="."
  program="${winrarPath}"
  commandline="${winrarSwitches} -z&quot;${sfxOptionsFileFullPath}&quot; -iicon&quot;${sfxIconFileFullPath}&quot; &quot;${archiveName}&quot; &quot;${archiveDirectoryFullPath}\${archiveFileMask}&quot;"
  workingdir="."
  failonerror="true" />
 </target>
</project>
To run this NAnt task:
  1. Open a command prompt in the targetDirectory you specified in the above build file.
  2. Run the build file with: nant -buildfile:<nameOfTheAboveBuildFile.build>

This will create a single executable file. When the user runs the executable, the setup files will be extracted and the "Setup.msi" (or whatever you specified in the sfx options file) will be executed.

Thursday, November 08, 2007

Getting Started with NAnt - .NET Build Tool

As part of working on a installer for .NET web application, I decided to use NAnt (stands for Not Ant) which is a .NET equivalent of the popular Java based ANT build tool. If you don't know what a build tool is, check out the Build Tool article on Wikipedia. The beauty of NAnt is in the fact that you can automate many tasks and once you have a working build file, you do not have to worry about it again. Here is some of you can do with the help of NAnt:

  • Install and register ASP.NET with IIS without user intervention
  • Start/stop/pause or install windows services
  • Zip or unzip files
  • Changes values inside text or xml files
  • Create files and/or directories
  • Copy/move files and/or directories
  • Set directory and/or file security permissions
  • Much more
  • If that is not enough, you can always extend NAnt with your own custom tasks (written in VB.NET or C#)
That being said, here are the steps to get started with NAnt:
  1. Get NAnt from http://prdownloads.sourceforge.net/nant/nant-0.85-bin.zip?download
  2. Get NAntContrib from http://prdownloads.sourceforge.net/nantcontrib/nantcontrib-0.85-bin.zip?download
  3. Extract NAnt in "C:\Program Files\NAnt"
  4. Extract NAntContrib\bin in "C:\Program Files\NAnt\bin"
  5. Create a NAnt.bat file in C:\Windows with the following:
    @echo off
    "C:\Program Files\NAnt\bin\nant.exe" %*
  6. Create and run a sample build file. Build files can perform many functions, from compiling your .NET application to downloading files from the net. For a list of Tasks, see the NAnt Tasks list and the NAntContrib Tasks list.
Below is a sample build file that takes a currently installed web application, pre-compilies it and creates a zip file of the pre-compiled application ready to be deployed:
<?xml version="1.0"?>
<project name="Ensemble" default="deploy" basedir="." xmlns="http://nant.sf.net/schemas/nant.xsd">
 <description>Precompiles and zips the Ensemble project</description>

  <property name="debug" value="true" overwrite="true" />

 <!-- The target directory where the application will be deployed and the zip file created -->
  <property name="targetDirectory" value="." overwrite="false" />

 <!-- The temporary directory where the web application will be precompiled -->
  <property name="deployTarget" value="${path::combine(path::get-full-path(targetDirectory), 'deploy')}" overwrite="true" />

  <!-- The virtual directory that the web application resides in -->
  <property name="virtualDirectory" value="webApplication" overwrite="true" />

 <!-- The name of the zip file to create when zipping the deployed web application -->
  <property name="deployZipFilename" value="webApplication.zip" overwrite="true" />

 <!-- The location where the zip file will be created -->
 <property name="deployZipFileLocation" value="${path::combine(path::get-full-path(targetDirectory), deployZipFilename)}" overwrite="true" />

 <!-- The location of the .NET Framework directory (for version 2) -->
  <property name="dotnetLocation" value="${framework::get-framework-directory('net-2.0')}" overwrite="true" />

  <!-- 'clean' target deletes the previously created zip file and deploy directory -->
  <target name="clean" description="Remove all generated files">
  <!-- Delete the existing zip file -->
    <delete file="${deployZipFileLocation}" if="${file::exists(deployZipFileLocation)}" />
  <!-- Delete the existing deploy directory -->
    <delete dir="${deployTarget}" if="${directory::exists(deployTarget)}" />
  </target>

  <!-- 'build' target precompiles this ASP.Net application into the deployTarget directory -->
  <target name="deploy" description="Precompiles the web application and creates a zip file for it" depends="clean">
    <!-- Precompile the web application with the built-in .NET utility -->
    <exec
     basedir="."
     program="${dotnetLocation}\aspnet_compiler.exe"
     commandline="-nologo -fixednames -v ${virtualDirectory} "${deployTarget}""
     workingdir="."
     failonerror="true" />

    <!-- Clean up the "deployTarget" directory -->
  <delete>
   <fileset>
    <!-- Delete any visual studio related files -->
    <include name="${deployTarget}/*.TempSolution" />
    <include name="${deployTarget}/**/*.scc" />
    <include name="${deployTarget}/**/*.resx" />
    <include name="${deployTarget}/**/*.txt" />
    <include name="${deployTarget}/**/*.db" />
    <include name="${deployTarget}/**/*.vssscc" />
    <!-- Delete all the files in the upload and files directory -->
    <include name="${deployTarget}/upload/**/*" />
    <include name="${deployTarget}/files/**/*" />
   </fileset>
  </delete>

    <!-- Create a zip file from precompiled web application -->
    <zip zipfile="${deployZipFileLocation}" includeemptydirs="true">
      <fileset basedir="${deployTarget}">
        <include name="**/*" />
      </fileset>
    </zip>

    <!-- delete the deployTarget directory -->
    <delete dir="${deployTarget}" />
  </target>
</project>