NAnt Tricks

From Hobowiki
(Redirected from Stupid NAnt Tricks)
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 <source lang="xml"> <target name="runTestsIfExist">

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

</source>

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. <source lang="xml"> <target name="delete-empty-files">

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

</source>

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: <source lang="vb"> 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 Try Dim stream As FileStream = File.OpenRead(location) reportContents = New [Byte](stream.Length-1) {} stream.Read(reportContents, 0, CInt(stream.Length)) stream.Close()

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

If Not (warnings Is Nothing) Then Dim warning As Warning For Each warning In warnings Console.WriteLine(Warning.Message) Next warning Else 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) ds0.Name="MyDataSource" 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) **

RS.SetReportParameters(fullpath,parameters)

'Set Snapshot Limit RS.SetReportHistoryLimit(fullpath,True,-1)

'Set History options Dim schedrefH As New NoSchedule RS.SetReportHistoryOptions(fullpath,True,False,schedrefH)


'Set Execution Options Dim execoption As ExecutionSettingEnum execoption = ExecutionSettingEnum.Live RS.SetExecutionOptions(fullpath,execoption,Nothing)

'Remove existing subscriptions Dim subscriptions As Subscription() = RS.ListSubscriptions(fullpath, Nothing) Dim subscrip As Subscription = Nothing For Each subscrip In subscriptions RS.DeleteSubscription(subscrip.SubscriptionID) Next subscrip

'Create subscriptions CreateSubscription1(fullpath)

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 = "[email protected]"

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 </source> 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: <source lang="xml"> <?xml version="1.0" encoding="utf-8" ?> <project xmlns="http://nant.sourceforge.net/release/0.86-beta1/nant.xsd" default="default" name="Report Deployment">

 <property name="ToolsDir" value="..\"/>
 <property name="WorkingDir" value="${project::get-base-directory()}" />
 <property name="RSCommand" value="${ToolsDir}Microsoft SQL\rs.exe" />
 <target name="default"
   description="The main target for full build process execution."
   depends="publishReports, publishSQL">
 </target>
 <target name="publishReports">
   <foreach item="File" property="filename">
     <in>
       <items basedir="${CCNetWorkingDirectory}">
         <include name="**/*.rss" />
       </items>
     </in>
     <do>
       <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="${reportfile}"" />
       </exec>
     </do>
   </foreach>
 </target>
 <target name="publishSQL">
   <foreach item="File" property="filename">
     <in>
       <items basedir="${CCNetWorkingDirectory}">
         <include name="**/*.sql"/>
       </items>
     </in>
     <do>
       <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}" />
       </exec>
     </do>
   </foreach>
 </target>

</project> </source> 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: <source lang="xml"> <project name="Deploy+Reports+to+Dev">

   <category>Reports</category>
   <triggers>
     <intervalTrigger seconds="60" buildCondition="IfModificationExists"/>
   </triggers>
   <artifactDirectory>&buildroot;\Reports\Artifacts</artifactDirectory>
   <workingDirectory>&buildroot;\Reports\</workingDirectory>
   <webURL>&webURLpre;Deploy+Reports+to+Dev&webURLpost;</webURL>
   <sourcecontrol type="multi">
     <sourceControls>
       <p4>
         <view>&p4view;/Reports/...</view>
         <executable>&p4path;</executable>
         <client>&p4client;</client>
         <applyLabel>false</applyLabel>
         <autoGetSource>true</autoGetSource>
       </p4>
     </sourceControls>
   </sourcecontrol>
   <tasks>
     <nant>
       <buildTimeoutSeconds>&buildtimeout;</buildTimeoutSeconds>
       <baseDirectory>.</baseDirectory>
       <executable>&nantpath;</executable>
       <buildArgs>&testssrs;</buildArgs>
       <buildFile>&buildroot;\Tools\BuildFiles\PublishReports.build</buildFile>
     </nant>
   </tasks>
   <publishers>
     <merge>
       <files>
         <file>&buildroot;\Reports\Artifacts\*.xml</file>
       </files>
     </merge>
     <xmllogger />
     <statistics />
     <email from="&emailfrom;" mailhost="&emailserver;" includeDetails="True">
       <users>
         <user name="Admin" address="[email protected]" group="DeployFailed" />
         <user name="Support Person" address="[email protected]" group="DeployFailed" />
       </users>
       <groups>
         <group name="BuildFailed" notification="Failed" />
       </groups>
     </email>
   </publishers>
   <externalLinks>
     <externalLink name="Code Repo" url="http://&webserver;/@md=d&cd=//depot/Reports&[email protected]//depot/Reports/?ac=83" />
   </externalLinks>
 </project>

</source>

<analytics uacct="UA-868295-1"></analytics>