I first learned continuous integration and build script principles from Steve Donie back in the last decade. I’m eternally grateful. To this day, the basic outline of the build scripts I deploy today have the same general flow that he taught me and implemented using NAnt and CruiseControl.net.
Today, we look back at the practice of forcing XML into a procedural programming language and chuckle at how naïve we were as an industry. Now, we use Powershell on the Windows platform for modern scripting. It was a brilliant move for James Kovacs to essentially port the build script concepts to powershell with the psake library.
I’ve been speaking on setting up basic software configuration management (SCM) at conferences and user groups for years, and I try to maintain an “Iteration Zero” Visual Studio solution template that includes scripting and the structure necessary for continuous integration right out of the gate. This build script is the one from that project, and it’s the template I use for every new software system. It’s been modified a bit over the years. It came from the one I had used for CodeCampServer back in the day, and it, of course, is used in every project we do at our software engineering practice at Clear Measure.
The full file can be found here.
# required parameters : # $databaseName Framework "4.0" properties { $projectName = "IterationZero" $unitTestAssembly = "UnitTests.dll" $integrationTestAssembly = "IntegrationTests.dll" $fullSystemTestAssembly = "FullSystemTests.dll"
$projectConfig = "Release" $base_dir = resolve-path . $source_dir = "$base_dirsrc" $nunitPath = "$source_dirpackagesNUnit.2.5.9.10348Tools" $build_dir = "$base_dirbuild" $test_dir = "$build_dirtest" $testCopyIgnorePath = "_ReSharper" $package_dir = "$build_dirpackage" $package_file = "$build_dirlatestVersion" + $projectName +"_Package.zip" $databaseName = $projectName $databaseServer = "localhostsqlexpress" $databaseScripts = "$source_dirCoreDatabase" $hibernateConfig = "$source_dirhibernate.cfg.xml" $schemaDatabaseName = $databaseName + "_schema" $connection_string = "server=$databaseserver;database=$databasename;Integrated Security=true;" $cassini_app = 'C:Program Files (x86)Common FilesMicrosoft SharedDevServer10.0WebDev.WebServer40.EXE' $port = 1234 $webapp_dir = "$source_dirUI" } task default -depends Init, CommonAssemblyInfo, Compile, RebuildDatabase, Test, LoadData task ci -depends Init, CommonAssemblyInfo, Compile, RebuildDatabase, Test, LoadData, Package task Init { delete_file $package_file delete_directory $build_dir create_directory $test_dir create_directory $build_dir } task ConnectionString { $connection_string = "server=$databaseserver;database=$databasename;Integrated Security=true;" write-host "Using connection string: $connection_string" poke-xml $hibernateConfig "//e:property[@name = 'connection.connection_string']" $connection_string @{"e" = "urn:nhibernate-configuration-2.2"} } task Compile -depends Init { msbuild /t:clean /v:q /nologo /p:Configuration=$projectConfig $source_dir$projectName.sln delete_file $error_dir msbuild /t:build /v:q /nologo /p:Configuration=$projectConfig $source_dir$projectName.sln } task Test { copy_all_assemblies_for_test $test_dir exec { & $nunitPathnunit-console.exe $test_dir$unitTestAssembly $test_dir$integrationTestAssembly /nologo /nodots /xml=$build_dirTestResult.xml } } task RebuildDatabase -depends ConnectionString { exec { & $base_diraliasqlaliasql.exe Rebuild $databaseServer $databaseName $databaseScripts } } task LoadData -depends ConnectionString, Compile, RebuildDatabase { exec { & $nunitPathnunit-console.exe $test_dir$integrationTestAssembly /include=DataLoader /nologo /nodots /xml=$build_dirDataLoadResult.xml } "Build failed - data load failure" } task CreateCompareSchema -depends SchemaConnectionString { exec { & $base_diraliasqlaliasql.exe Rebuild $databaseServer $schemaDatabaseName $databaseScripts } } task SchemaConnectionString { $connection_string = "server=$databaseserver;database=$schemaDatabaseName;Integrated Security=true;" write-host "Using connection string: $connection_string" poke-xml $hibernateConfig "//e:property[@name = 'connection.connection_string']" $connection_string @{"e" = "urn:nhibernate-configuration-2.2"} } task CommonAssemblyInfo { $version = "1.0.0.0" create-commonAssemblyInfo "$version" $projectName "$source_dirCommonAssemblyInfo.cs" } task Package -depends Compile { delete_directory $package_dir #web app copy_website_files "$webapp_dir" "$package_dirweb" copy_files "$databaseScripts" "$package_dirdatabase" zip_directory $package_dir $package_file } task FullSystemTests -depends Compile, RebuildDatabase { copy_all_assemblies_for_test $test_dir &$cassini_app "/port:$port" "/path:$webapp_dir" & $nunitPathnunit-console-x86.exe $test_dir$fullSystemTestAssembly /framework=net-4.0 /nologo /nodots /xml=$build_dirFullSystemTestResult.xml exec { taskkill /F /IM WebDev.WebServer40.EXE } } function global:zip_directory($directory,$file) { write-host "Zipping folder: " $test_assembly delete_file $file cd $directory & "$base_dir7zip7za.exe" a -mx=9 -r $file cd $base_dir } function global:copy_website_files($source,$destination){ $exclude = @('*.user','*.dtd','*.tt','*.cs','*.csproj','*.orig', '*.log') copy_files $source $destination $exclude delete_directory "$destinationobj" } function global:copy_files($source,$destination,$exclude=@()){ create_directory $destination Get-ChildItem $source -Recurse -Exclude $exclude | Copy-Item -Destination {Join-Path $destination $_.FullName.Substring($source.length)} } function global:Copy_and_flatten ($source,$filter,$dest) { ls $source -filter $filter -r | Where-Object{!$_.FullName.Contains("$testCopyIgnorePath") -and !$_.FullName.Contains("packages") }| cp -dest $dest -force } function global:copy_all_assemblies_for_test($destination){ create_directory $destination Copy_and_flatten $source_dir *.exe $destination Copy_and_flatten $source_dir *.dll $destination Copy_and_flatten $source_dir *.config $destination Copy_and_flatten $source_dir *.xml $destination Copy_and_flatten $source_dir *.pdb $destination Copy_and_flatten $source_dir *.sql $destination Copy_and_flatten $source_dir *.xlsx $destination } function global:delete_file($file) { if($file) { remove-item $file -force -ErrorAction SilentlyContinue | out-null } } function global:delete_directory($directory_name) { rd $directory_name -recurse -force -ErrorAction SilentlyContinue | out-null } function global:delete_files_in_dir($dir) { get-childitem $dir -recurse | foreach ($_) {remove-item $_.fullname} } function global:create_directory($directory_name) { mkdir $directory_name -ErrorAction SilentlyContinue | out-null } function global:create-commonAssemblyInfo($version,$applicationName,$filename) { "using System; using System.Reflection; using System.Runtime.InteropServices; //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. // Runtime Version:2.0.50727.4927 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ [assembly: ComVisibleAttribute(false)] [assembly: AssemblyVersionAttribute(""$version"")] [assembly: AssemblyFileVersionAttribute(""$version"")] [assembly: AssemblyCopyrightAttribute(""Copyright 2010"")] [assembly: AssemblyProductAttribute(""$applicationName"")] [assembly: AssemblyCompanyAttribute(""Headspring"")] [assembly: AssemblyConfigurationAttribute(""release"")] [assembly: AssemblyInformationalVersionAttribute(""$version"")]" | out-file $filename -encoding "ASCII" } function script:poke-xml($filePath, $xpath, $value, $namespaces = @{}) { [xml] $fileXml = Get-Content $filePath if($namespaces -ne $null -and $namespaces.Count -gt 0) { $ns = New-Object Xml.XmlNamespaceManager $fileXml.NameTable $namespaces.GetEnumerator() | %{ $ns.AddNamespace($_.Key,$_.Value) } $node = $fileXml.SelectSingleNode($xpath,$ns) } else { $node = $fileXml.SelectSingleNode($xpath) } Assert ($node -ne $null) "could not find node @ $xpath" if($node.NodeType -eq "Element") { $node.InnerText = $value } else { $node.Value = $value } $fileXml.Save($filePath) }