NAnt Tricks

From Hobowiki
Revision as of 10:39, 20 April 2021 by Brendan (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

If you've ever worked with automated build software, or even done your own automation of build scripts, you'll know that it can be quite a bit of a science. In my short period of time working with this tool called NAnt I have used a couple cool parts of this utility to make things easier for me. Since NAnt primarily works on an XML file with multiple targets to achieve a result, I've mostly just written the code in terms of a single target or two. Here they are:

Running Arbitrary Code on Sets of Files

Here's some example code that will run through a set of files contained in the "CCNetWorkingDirectory" property path, find the files with the .sql extension and output the full path to each file. Just in case you are wondering, yes this script came about from being used in conjunction with CruiseControl.NET

<target name="runTestsIfExist">
    <foreach item="File" property="sqlfile">
        <items basedir="${CCNetWorkingDirectory}">
          <include name="**/*.sql"/>
        <script language="C#">
              public static void ScriptMain(Project p)
                  p.Log(Level.Error, "You are working with the file: " + p.Properties["sqlfile"]);

Deleting Zero Length Files In A Directory

Here's some example code that will run through a set of files contained in the "CCNetWorkingDirectory" property path, in addition to any sub-directory, find the files with the .jpg extension delete all the files with a zero size.

<target name="delete-empty-files">
    <foreach item="File" property="delfile">
        <items basedir="${CCNetWorkingDirectory}">
          <include name="**/*.jpg"/>
        <property name="size" value="${file::get-length(delfile)}"/>
        <delete if="${int::parse(size) == 0}" file="${delfile}" verbose="true"/>

Deploying SQL Server Reporting Services (SSRS) Reports

Now, I know what you are thinking, why in the hell would you want to use NAnt to deploy SSRS? Well, the answer to that question is that you probably won't need to. Then your follow up question would be, "Well then why the hell are you showing me how to do this, you're stupid and I hate you."

All banter aside, deploying SSRS reports is fairly easy to do with Visual Studio 2005, or just using some of the SQL Server deployment utilities. The time when I noticed that we needed something more, was when I realized that a report for SSRS sometimes has extra info that isn't included in the meat of a report, and is just as necessary to be deployed to number of different environments (e.g. dev, test, stage, prod). The reason I used this, was that my reports needed to be able to roll out along with subscriptions, and a separate configuration file to give all that info to SSRS. Now, I must warn you this is not for the faint of heart, if you doubt your scripting skills, turn back mere mortal!

First, you'll need RSScripter, this will let you automatically create the bulk of the info for your deployed reports. It goes 95% of the way there, you just need to change and add a couple lines to make this work. Here's an example report deployment script as output by RSScripter, certain lines have been modified by me:

Public Sub Main()
	Dim name As String = "A Daily Report"
	Dim parent As String = "/Reports"
	Dim location As String = arglocation ' 'line modified to allow script to pass in report name
	Dim overwrite As Boolean = True
	Dim reportContents As Byte() = Nothing
	Dim warnings As Warning() = Nothing
	Dim fullpath As String = parent + "/" + name

	'Common CatalogItem properties
	Dim descprop As New [Property]
	descprop.Name = "Description"
	descprop.Value = "A Daily Report Subscription."
	Dim hiddenprop As New [Property]
	hiddenprop.Name = "Hidden"
	hiddenprop.Value = "False"

	Dim props(1) As [Property]
	props(0) = descprop
	props(1) = hiddenprop

	'Read RDL definition from disk
		Dim stream As FileStream = File.OpenRead(location)
		reportContents = New [Byte](stream.Length-1) {}
		stream.Read(reportContents, 0, CInt(stream.Length))

		warnings = RS.CreateReport(name, parent, overwrite, reportContents, props)

		If Not (warnings Is Nothing) Then
			Dim warning As Warning
			For Each warning In warnings
			Next warning
			Console.WriteLine("Report: {0} published successfully with no warnings", name)
		End If

		'Set report DataSource references
		Dim dataSources(0) As DataSource

		Dim dsr0 As New DataSourceReference
		dsr0.Reference = "/Data Sources/MyDataSource"
		Dim ds0 As New DataSource
		ds0.Item = CType(dsr0, DataSourceDefinitionOrReference)
		dataSources(0) = ds0

		RS.SetItemDataSources(fullpath, dataSources)

		Console.Writeline("Report DataSources set successfully")

		'Set Report Parameters
		Dim parameters(2) As ReportParameter

		** SNIP (excessively long and not useful) **


		'Set Snapshot Limit

		'Set History options
		Dim schedrefH As New NoSchedule

		'Set Execution Options
		Dim execoption As ExecutionSettingEnum
		execoption = ExecutionSettingEnum.Live
		'Remove existing subscriptions
		Dim subscriptions As Subscription() = RS.ListSubscriptions(fullpath, Nothing)
		Dim subscrip As Subscription = Nothing
		For Each subscrip In subscriptions
		Next subscrip
		'Create subscriptions

 Catch e As IOException   ' lines modified to ensure exceptions are thrown all the way to CCNet dashboard.
		Throw New System.Exception("Error : " + e.Message + Environment.NewLine + "Report: " + arglocation)
	Catch e As SoapException
		Throw New System.Exception("Error : " + e.Detail.Item("ErrorCode").InnerText + " (" + e.Detail.Item("Message").InnerText + ") " + Environment.NewLine + "Report: " + arglocation)
	End Try
End Sub

Private Sub CreateSubscription1(ByVal fullpath As String)

	Dim report As String = fullpath
	Dim desc As String = "Send e-mail to recipients"
	Dim eventType As String = "TimedSubscription"

	Dim parameters(2) As ParameterValue

	Dim parameter1 As New ParameterValue()
	parameter1.Name = "SortBy"
	parameter1.Value = ""
	parameters(0) = parameter1

	Dim parameter2 As New ParameterValue()
	parameter2.Name = "Filter"
	parameter2.Value = "Things"
	parameters(1) = parameter2

	Dim parameter3 As New ParameterValue()
	parameter3.Name = "GroupBy"
	parameter3.Value = "Some Guy"
	parameters(2) = parameter3

	Dim extensionParams(8) As ParameterValueOrFieldReference

	extensionParams(0) = New ParameterValue()
	CType(extensionParams(0),ParameterValue).Name = "TO"
	CType(extensionParams(0),ParameterValue).Label = ""
	CType(extensionParams(0),ParameterValue).Value = ""

	Dim extSettings As New ExtensionSettings()
	extSettings.ParameterValues = extensionParams
	extSettings.Extension = "Report Server Email"

	** SNIP (excessively long and not useful) **

	rs.CreateSubscription(report, extSettings, desc, eventType, matchData, parameters)

	Console.WriteLine("Subscription1 created successfully")

End Sub

This script was modified in a few places to make it work with my NAnt script.

  • Changed line with Dim location As String to Dim location As String = arglocation
    • This was done to allow the NAnt script to feed it the filename, in case the location or name of the file changed, this script wouldn't be affected.
  • Changed lines with exception handling
    • Now the script will re-throw any errors encountered, so that NAnt will stop and log that an error has happened
    • This makes sure CruiseControl, or other system running the NAnt script knows about the exception.
  • Added code to delete all existing subscriptions from the report if they exist
    • If existing subscriptions aren't deleted, this script will try to add new ones each time, which is bad. (Of course there are other ways to do this, but this is the most optimal way to maintain subscription integrity.)

Now for the NAnt Script that deploys this script:

<?xml version="1.0" encoding="utf-8" ?>
<project xmlns="" default="default" name="Report Deployment">
		This script is configured as though it is part of a standard build system.
		-ToolsDir is the directory above the current one, assuming this script is run directly.
		-WorkingDir is the root directory containing the RDL and SQL files to be run.
		-RSCommand is the location of the reporting services deployment command line utility.
  <property name="ToolsDir" value="..\"/>
  <property name="WorkingDir" value="${project::get-base-directory()}" />
  <property name="RSCommand" value="${ToolsDir}Microsoft SQL\rs.exe" />

		This is called automatically when no targets are specified, it deploys the reports and SQL to the specified server.
  <target name="default"
    description="The main target for full build process execution."
    depends="publishReports, publishSQL">

		Deploy all report files in the working directory to the specified report server.
  <target name="publishReports">
    <foreach item="File" property="filename">
        <items basedir="${CCNetWorkingDirectory}">
          <include name="**/*.rss" />
		The .RDL report file property is set to insure that non-linked reports know where the report is currently located.
		-ReportServer is the fully qualified name of the report server.
        <property name="reportfile" value="${string::substring(filename,0,string::get-length(filename)-4)}"/>
        <echo message="Publishing report using script: ${filename}" verbose="true"/>
        <exec program="${RSCommand}" failonerror="true">
          <arg value="-i" />
          <arg value="${filename}" />
          <arg value="-s" />
          <arg value="http://${ReportServer}/ReportServer" />
          <arg value="-e" />
          <arg value="Mgmt2005" />
          <arg value="-l" />
          <arg value="60" />
          <arg value="-v" />
          <arg value="arglocation=&quot;${reportfile}&quot;" />

		Execute the SQL contained in the report project directory in no particular order.
		-CCNetWorkingDirectory is the working directory that CCNet sets automatically.
		-SQLServer is the fully qualified name of the SQL Server to run the SQL commands on.
		-Database is the database on the SQL Server to run the SQL files in the direectory.
  <target name="publishSQL">
    <foreach item="File" property="filename">
        <items basedir="${CCNetWorkingDirectory}">
          <include name="**/*.sql"/>
        <echo message="Executing SQL: ${filename}" verbose="true"/>
        <exec program="sqlcmd.exe" failonerror="true">
          <arg value="-S" />
          <arg value="${SQLServer}" />
          <arg value="-d" />
          <arg value="${Database}" />
          <arg value="-E" />
          <arg value="-b" />
          <arg value="-i" />
          <arg value="${filename}" />

This NAnt script will deploy the report using RS and then follow it up with deploying any SQL related to that report that happens to be contained in the directory with the report. As you can also see, those of you who do know NAnt, will recognize that most of what this script executes is derived from properties passed into this script from another script. (In this case, CruiseControl.NET again.) One of my favorite things about this approach is that if you tie CruiseControl.NET into a source control provider, that contains all your reports, and then you use this method for deploying your reports, you will get some very useful info about all of your reports and if and when any of them failed to deploy.

Just in case you are wondering how the CruiseControl config looks for one of these items, here it is:

<project name="Deploy+Reports+to+Dev">
      <intervalTrigger seconds="60" buildCondition="IfModificationExists"/>
    <sourcecontrol type="multi">
      <xmllogger />
      <statistics />
      <email from="&emailfrom;" mailhost="&emailserver;" includeDetails="True">
          <user name="Admin" address="" group="DeployFailed" />
          <user name="Support Person" address="" group="DeployFailed" />
          <group name="BuildFailed" notification="Failed" />
      <externalLink name="Code Repo" url="http://&webserver;/@md&#61;d&amp;cd&#61;//depot/Reports&amp;c&#61;6Pv@//depot/Reports/?ac&#61;83" />