« SmartPart and AjaxBasePart get Together | Main | Remote Service Deployment with VS.NET PowerShell Build Events »

Visual Studio Build Events using PowerShell

See the bug?

UPDATE #4: Just found another nice looking editor PowerShellIDE.

UPDATE #3: PowerShell team responded and their guidance to avoid issues with CMD messing up the arguments passed to the PowerShell script is to use single quotes around the PowerShell arguments.  I updated the build event example below.

UPDATE #2: Fixed a minor bug in the build DeploymentScript.ps1.  It was starting to parse the "Additional Args" at an index of 3 when it should have starting at 4. 

UPDATE #1: Forgot to link to PowerShell Analyzer.  A nice tool for working with PowerShell, have not played with it a ton yet, but it looks promising.

Working in a team with multiple developers and different environments often introduces the need for multiple deployment configurations.

There are quite a few interesting tools out there for advanced deployment configuration tasks:

In my case I just need something simple that each developers on my team (currently only 4-5 regular contributors) can use and customize.  In the past I have used Windows Scripting Host scripts written in JScript to perform my post build events but with the release of PowerShell I was interested in finding a task that would afford me the excuse of learning a new technology (you can never know too many).

My solution at the moment is a PowerShell script that loads information from an XML file based on Project, Build Configuration, User Name, and Build Event Type (Pre or Post). The XML file is essentially a list of PowerShell scripts depending on the passed in variables it loads the proper PowerShell script and executes it.

<tangent degree="Minor">

For me PowerShell has a steep learning curve and it's taking me a little while to wrap my head around.  It took me about a day to figure out how to write the script for this post just because of the lack of relevant examples online and my lack of familiarity with the command syntax.

I'm really excited about how it can be extended with the use of custom CmdLets; a CmdLet pack that supported common Visual Studio Team Foundation activities like TF.exe would add a lot of value of the type of scenario that I am playing with here.  I am also interested in finding CmdLets or information on how to perform remote command execution (executing CMD or PowerShell commands on remote machines). Anyone know of examples or projects that cover these scenarios?

</tangent>

Onto the examples (I know what you are here for)...

Pre/Post Build Event in Visual Studio .NET
A simple example of the actually CMD line that is executed as part of Visual Studio's configured post-build event. Use single quotes for the arguments that should be passed to the PowerShell script.  That will avoid CMD causing any problems by the way it handles double quoted string. Note that for some reason the quotes that come immediately after a Visual Studio variable that ends with a \ have to have an additional \ placed in front of them.  I have communicated with the PowerShell team about this and believe that they are aware of the behavior.

The first 5 arguments are required and are used to lookup the script that should be executed.  Additional argument are optional and can be configured on a per project basis, they are loaded into a Hashtable using a (key)=(value) format and are used to replace variables in the executed script.

powershell.exe "$(SolutionDir)DeployScript.ps1" 'Post' '$(SolutionDir)' '$(TargetFileName)' '$(ConfigurationName)' 'targetDir=$(TargetDir)' 'projectDir=$(ProjectDir)'

DeployConfig.xsd
The documented schema that can be used to validate DeployConfig.xml.  Word wrap makes it a little messy.. sorry about that.

<?xml version="1.0" encoding="UTF-8"?>

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">

   <xs:element name="configuration">

      <xs:annotation>

         <xs:documentation>The root element of the DeployConfig XML document used by the DeployScript.ps1 PowerShell script.</xs:documentation>

      </xs:annotation>

      <xs:complexType>

         <xs:sequence>

            <xs:element name="project" maxOccurs="unbounded">

               <xs:annotation>

                  <xs:documentation>Each project that is being configured with a build event will have it's own project node</xs:documentation>

               </xs:annotation>

               <xs:complexType>

                  <xs:sequence>

                     <xs:element name="buildConfig" maxOccurs="unbounded">

                        <xs:annotation>

                           <xs:documentation>Each build configuratoin (ie. Debug, Release) should have it's own buildConfig node.</xs:documentation>

                        </xs:annotation>

                        <xs:complexType>

                           <xs:sequence>

                              <xs:element name="user" maxOccurs="unbounded">

                                 <xs:annotation>

                                    <xs:documentation>Each user can be configured to execute a different PowerShell script by creating a new user node that specifies their Active Directory username (eg. domain\user).  If no node is found for the current user DeployScript.ps1 will search for a user node with the name attribute set to 'default'</xs:documentation>

                                 </xs:annotation>

                                 <xs:complexType>

                                    <xs:sequence>

                                       <xs:element name="buildEvent" maxOccurs="2">

                                          <xs:annotation>

                                             <xs:documentation>Each user can have two build events specified for each project and build configuration.  One can be executed on pre-build and one on post-build.  The text for this node is the actual script that will be executed on build.  It is recommended that the commands within this node be wrapped with CDATA.</xs:documentation>

                                          </xs:annotation>

                                          <xs:complexType>

                                             <xs:simpleContent>

                                                <xs:extension base="xs:string">

                                                   <xs:attribute name="name" use="required">

                                                      <xs:annotation>

                                                         <xs:documentation>Possible values are 'Pre' and 'Post' depending on when the event should be executed.</xs:documentation>

                                                      </xs:annotation>

                                                      <xs:simpleType>

                                                         <xs:restriction base="xs:string">

                                                            <xs:enumeration value="Pre"/>

                                                            <xs:enumeration value="Post"/>

                                                         </xs:restriction>

                                                      </xs:simpleType>

                                                   </xs:attribute>

                                                </xs:extension>

                                             </xs:simpleContent>

                                          </xs:complexType>

                                       </xs:element>

                                    </xs:sequence>

                                    <xs:attribute name="name" type="xs:string" use="required">

                                       <xs:annotation>

                                          <xs:documentation>The name of the user that this build script applies to.  Use 'default' to specify a default script that should be run if the current username is not found.</xs:documentation>

                                       </xs:annotation>

                                    </xs:attribute>

                                 </xs:complexType>

                              </xs:element>

                           </xs:sequence>

                           <xs:attribute name="name" type="xs:string" use="required">

                              <xs:annotation>

                                 <xs:documentation>The Build Configuration of the executing project build.  Visual Studio Build Event Variable: $(ConfigurationName)</xs:documentation>

                              </xs:annotation>

                           </xs:attribute>

                        </xs:complexType>

                     </xs:element>

                  </xs:sequence>

                  <xs:attribute name="targetFileName" type="xs:string" use="required">

                     <xs:annotation>

                        <xs:documentation>The Target File Name of the project that the build event is being configured for. Visual Studio Build Event Variable: $(TargetFileName)</xs:documentation>

                     </xs:annotation>

                  </xs:attribute>

               </xs:complexType>

            </xs:element>

         </xs:sequence>

      </xs:complexType>

   </xs:element>

</xs:schema>

DeployConfig.xml
This is a simple example of a post build event for a sample project using the Visual Studio Post Build Event example above.  Note that the optional arguments in the Post Build Event are used to populate the actual command using string.Replace.  Keys from the optional arguments Hashtable are searched for (encapsulated in {}) and replaced with the value from the Hashtable.

<?xml version="1.0" encoding="utf-8"?>

<configuration>

   <project targetFileName="Namespace.Sample.dll">

      <buildConfig name="Debug">

         <user name="domain\username">

            <buildEvent name="Post"><![CDATA[remove-item -path "\\server\c$\Inetpub\wwwroot\bin\*.pdb", "\\server\c$\Inetpub\wwwroot\bin\*.dll", "\\server\c$\Inetpub\wwwroot\*.svc", "\\server\c$\Inetpub\wwwroot\*.config" -force

   copy-item -path "{targetDir}*.dll","{targetDir}*.pdb" -destination "\\server\c$\Inetpub\wwwroot\bin" -force

   copy-item -path "{projectDir}*.config","{projectDir}*.svc" -destination "\\meservices01\c$\Inetpub\wwwroot" -force]]></buildEvent>

         </user>

      </buildConfig>

   </project>

</configuration>

DeployScript.ps1
The PowerShell script that is executed during the Pre/Post build event.  I invite anyone with good ideas to modify this script and repost it to their blog with a link back to this post. Join the conversation and help me make this script better!

trap {

   $errorTxt = $_.Exception.Message

   write-output "$errorTxt"

   break

}

 

$buildEvent = $args[0]

$solutionDir = $args[1]

$targetFile = $args[2]

$configName = $args[3]

 

write-output ""

write-output "*******************************************"

write-output "Required Args:"

write-output ""

write-output "Build Event:         $buildEvent"

write-output "Configuration Name:      $configName"

write-output "Target File Name:      $targetFile"

write-output "Solution Directory:      $solutionDir"

write-output "*******************************************"

write-output "Additional Args:"

write-output ""

 

$argsHash = @{}

for($x=4; $x -lt $args.Length; $x++) {

   $curArg = $args[$x]

   $curArgSplit = $args[$x].split("=")

   $argsHash.add($curArgSplit[0], $curArgSplit[1]);

   write-output "$curArg"

}

 

write-output "*******************************************"

write-output ""

write-output "Deployment Started"

 

$configPath = (join-path $solutionDir "DeployConfig.xml")

[xml]$configDoc = get-content $configPath

write-output "Configuration Loaded from $configPath"

 

$projNode = $configDoc.configuration.project | ?{$_.targetFileName -eq $targetFile}

if($projNode -ne $null) {

   write-output "Project Node Found: $targetFile"

}

else {

   throw "No Project node could be found: $targetFile";

}

 

$configNode = $projNode.buildConfig | ?{$_.name -eq $configName}

 

if($configNode -ne $null) {

   write-output "Build Config Node found: $configName"

}

else {

   throw "No Build Config node could be found: $configName";

}

 

#$compName = (get-wmiobject win32_computersystem).Name

$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name

$userNode = $configNode.user | ?{$_.name -eq $currentUser}

 

if($userNode -eq $null) {

   write-output "User Node not found for $currentUser"

   $userNode = $configNode.user | ?{$_.name -eq 'default'}

 

   if($userNode -ne $null) {

      write-output "User Node found: default"

   }

   else {

      throw "No User Node (named or default) could be found: $currentUser"

   }

}

else {

   write-output "User Node found: $currentUser"

}

 

$buildEventNode = $userNode.buildEvent | ?{$_.name -eq $buildEvent}

 

if($buildEventNode -eq $null) {

   throw "No build event could be found for the current build event: $buildEvent"

}

else {

   write-output "Build Event Node found: $buildEvent"

}

 

$deployCmd = $buildEventNode.get_InnerText()

 

foreach($arg in $argsHash.Keys) {

   $deployCmd = $deployCmd.Replace("{$arg}", $argsHash[$arg]);

}

 

write-output "Executing Command:"

invoke-expression $deployCmd

 

write-output "Deployment Ended"

TrackBack

TrackBack URL for this entry:
http://www.capdes.com/mt/mt-tb.cgi/12

Listed below are links to weblogs that reference Visual Studio Build Events using PowerShell:

» Remote Service Deployment with VS.NET PowerShell Build Events from Capital Design
This post is a follow up to "Visual Studio Build Events using PowerShell". When developing Windows Services that are deployed on a remote machine within your development environment it can be very annoying to have to stop and start the... [Read More]

» Zoo takes measures to prevent from vinyl coated fencing
Since then, a 4-foot high vinyl-coated wire barricade has been added to surround the entire tiger exhibit, which was built [Read More]

» SIRIUS Satellite Radio to Observe from rv rental washington
SIRIUS products for the car, truck, home, RV and boat are available in more than 20000 retail locations, including Best [Read More]

» How do you say Rococo from deal disneyland package paris
It’s like calling ‘Paris’ in Vegas … well, Paris. The manager of the project insists, however, that it’ll be a [Read More]

» acai berry weightloss from acai berry weightloss
situation test public marry easy gentleman before large twice! krigopizp aim strong selection expensive engineering circumstance die? [Read More]

About

View Eric Schoonover's profile on LinkedIn
 

Follow me on Twitter
 
© Copyright 2007, Eric Schoonover

Search

Post Info

This page contains a single entry from the blog posted on March 6, 2007 2:58 AM.

The previous post in this blog was SmartPart and AjaxBasePart get Together.

The next post in this blog is Remote Service Deployment with VS.NET PowerShell Build Events.

Many more can be found on the main index page or by looking through the archives.

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.