Prepping - or 'instrumenting' - your projects for NuGet can save you a lot of aggro in the long term but can be an almighty pain to do. There are several areas you have to cover for each project you want to automatically package:
- Create a nupack.ps1 file at the solution level which will package a given project
- Creating the post-build scripts which invoke the script above
- Managing dependencies between packages.
So, being a resourceful (i.e. lazy) type, I decided that it was about time to bring PowerShell to my rescue yet again. I wanted a script that would do the following to a complete multi-project Visual Studio solution:
- Create the nupack.ps1 file at the solution level
- Amend each project to call this file after the build has completed
- Run nuget spec against every single project in the solution to create the packaging specification
- Interrogate each project file for dependencies and map these to package inter-dependencies.
The bits in ellipses are where you would normally insert information about your package, but the <dependencies> tag allows you to specify the packages which must also be installed. Our job is to make sure that this file is properly constituted. If, while we're at it, we can get the other tags properly populated as well then that would be a bonus. So, let's write a PowerShell script to do just that!$id$ $version$ ... ... ... ... .../ ... false to be supplied Summary of changes made in this release of the package. Tag1 Tag2 The
The Script
The PowerShell script to do all this looks like:#NuGetProjects
#Prepares a set of projects in a solution for NuGet processing
#Generates a nuspec.ps1 file in the solution's root folder
#then modifies each VBproj file to run this as part of its postbuild event
#Also generates .nuspec files for each project and updates the dependencies for each of these files
param(
[string]$solutionDir = '.'
)
#transfer the info from the VB project file to the nuspec file where we can
#mangle it to our heart's content
Function transferprojectinfo
{
param (
[System.IO.FileInfo]$projectFile,
[xml]$nuspecxml
)
#register the MSBuild namespace so we can search the project file
$ns = @{e='http://schemas.microsoft.com/developer/msbuild/2003'}
#load up the project XML
$xml = New-Object System.Xml.XmlDocument
$xml.Load($projectFile.FullName)
[string]$infoFileName = ""
$descNode = (select-xml -namespace $ns -xml $xml -XPath '//e:AssemblyName').node
$nuspecXML.package.metadata.description = $descNode.InnerText
Push-Location $projectFile.Directory
if (Test-Path (Join-Path $projectFile.Directory "\AssemblyInfo.vb"))
{
$infoFileName = (join-path $projectFile.Directory "AssemblyInfo.vb")
}
else
{
if (Test-Path (Join-Path $projectFile.Directory "\My Project\AssemblyInfo.vb"))
{
$infoFileName = (Join-Path $projectFile.Directory "\My Project\AssemblyInfo.vb")
}
}
if ($infoFileName -ne "")
{
Write-Output "AssemblyInfo.VB location ='$infoFileName'"
[string]$assemblyDesc =( Get-Content $infoFileName|Select-String -Pattern '\<assembly: assemblydescription="" desc="">.*)"\)\> ' |%{$_.matches}|%{$_.groups["desc"].value})
[string]$assemblyTitle = ( Get-Content $infoFileName|Select-String -Pattern '\<assembly: assemblytitle="" desc="">.*)"\)\> ' |%{$_.matches}|%{$_.groups["desc"].value})
[string]$assemblyCompany = ( Get-Content $infoFileName|Select-String -Pattern '\<assembly: assemblycompany="" desc="">.*)"\)\> ' |%{$_.matches}|%{$_.groups["desc"].value})
[string]$assemblyVersion = ( Get-Content $infoFileName|Select-String -Pattern '\<assembly: assemblyversion="" desc="">.*)"\)\> ' |%{$_.matches}|%{$_.groups["desc"].value})
if($assemblyDesc -ne '')
{
$nuspecxml.package.metadata.description = $assemblyDesc
}
else
{
$nuspecxml.package.metadata.description = "to be supplied"
}
$nuspecxml.package.metadata.title= $assemblyTitle
$nuspecxml.package.metadata.copyright = $assemblyCompany
}
#now do the dependencies
$dependencyNodes = (select-xml -namespace $ns -xml $xml -XPath '/e:Project/e:ItemGroup/e:ProjectReference/e:Name')
$nuspecxml.dependencies.RemoveElement
$depRoot = $nuspecxml.CreateElement("dependencies")
foreach($depNode in $dependencyNodes)
{
$newDep = $nuspecXML.CreateElement("dependency")
$newDep.SetAttribute("id",$depNode.Node.InnerText)
$newDep.SetAttribute("version", "1.0.0.0")
$depRoot.AppendChild($newDep)
}
$nuspecxml.package.metadata.AppendChild($depRoot)
Pop-Location
}
Function makespec
{
param([string]$dirName)
Write-Output "Processing project directory $dirName"
pushd $dirName
$projects = get-childitem -Filter *.vbproj
foreach ($proj in $projects)
{
Write-Output "Processing directory $proj"
$dirName = $proj.DirectoryName
$nuspecFilename = (Join-Path $dirname *.nuspec)
$nuspecFiles = (Get-ChildItem $nuspecFilename)
foreach($nuspecFile in $nuspecFiles)
{
if(!(test-path $nuspecFile))
{
nuget spec -f $proj
}
$nuspecxml = [xml](Get-Content $nuspecFile)
$metadata = $nuspecxml.package.metadata
$metadata.authors = 'authorname'
$metadata.owners = 'authorname'
$metadata.authors = 'companyname'
$metadata.licenseUrl = 'http://servername/sitename'
$metadata.projectUrl = ''http://servername/sitename'
$metadata.iconUrl = ''http://servername/sitename/image.gif'
transferprojectinfo $proj $nuspecxml
$nuspecxml.Save($nuspecFile.FullName)
Write-Output "Fixed up $($nuspecFile.Fullname)"
}
}
popd
}
#constant definitions
[string]$repositoryLocation = "\\servername\packagesharename"
#this is the main processing part of the script
Write-Output '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
Write-Output "Preparing to process projects in directory tree $($solutionDir) "
$buildEventText = 'Powershell.exe $(SolutionDir)nupack.ps1 -projectdir $(ProjectDir) -config $(ConfigurationName) -outdir $(OutDir)'
$ErrorActionPreference = "Stop"
Write-Output "Setting Context to $($solutionDir)"
pushd $solutionDir
#extract the leaf folder of the directory
$slnFolder = (join-path $repositoryLocation (Get-Item $solutionDir).Name)
#create the nuget target directory if it doesn't exist
if(!(Test-Path $slnFolder))
{
mkdir $slnFolder
}
#now generate the post-build script : note use of the substitution variables
Write-Output "generating nupack.ps1 file"
echo '
#PowerShell script invoked
param(
[string]$projectdir,
[string]$config="Debug",
[string]$outDir
)
if($config -eq "Release")
{
write-output "Project Directory = $projectdir"
pushd $projectdir
write-output "Deleting the packages..."
del $projectdir\*.nupkg
write-output "Repackaging..."
nuget.exe pack -Properties OutDir=$outDir
popd' > nupack.ps1
"xcopy /Y /C `"`$projectdir*.nupkg`" `"$slnFolder`"" >> nupack.ps1
'}' >> nupack.ps1
$projects = get-childitem -Filter *.vbproj -Recurse
foreach ($proj in $projects)
{
#first, generate the nuget files
#this assumes that we have nuget in the path
Write-Output "Preparing to process projects in directory tree '$($proj.DirectoryName)' "
pushd $proj.DirectoryName
nuget spec -f
Write-Output "Processing project $($proj.FullName)"
$ns = @{e='http://schemas.microsoft.com/developer/msbuild/2003'}
#$xml = [System.Xml.XmlDocument](get-content $proj.FullName);
$xml = New-Object System.Xml.XmlDocument
$xml.Load($proj.FullName)
$node = (select-xml -namespace $ns -xml $xml -XPath '//e:PostBuildEvent').node
$outputType =(select-xml -namespace $ns -xml $xml -XPath '//e:OutputType').node.InnerText
if ($outputType -eq 'Library')
{
Write-Output "Making the specification for $($proj.DirectoryName)"
makespec $proj.DirectoryName
if ($node -ne $null)
#found that there's a PostBuildEvent node already
{
$node.InnerText = $buildEventText
}
else
{
#create the node
$projectNode = (select-xml -namespace $ns -xml $xml -XPath '/e:Project').node
$pgElement = $xml.CreateElement('PropertyGroup', 'http://schemas.microsoft.com/developer/msbuild/2003')
$pbeElement = $xml.CreateElement('PostBuildEvent', 'http://schemas.microsoft.com/developer/msbuild/2003')
$pbeElement.InnerText = $buildEventText
$pgElement.AppendChild($pbeElement)
$projectNode.AppendChild($pgElement)
}
Set-ItemProperty $proj -name IsReadOnly -value $false
$xml.Save($proj.FullName)
Write-Output "Updated $proj.FullName"
}
}
Write-Output "Finished processing projects in $solutionDir"
popd
It's a hell of a lot simpler than it looks. You simply copy it to the solution directory and run it, or run it from where it currently resides but passing the solutions folder name as a parameter. It traverses the solutions folder structure and amends every single .NET project according to the four steps above. It also creates the nupack.ps1 script at the solution level. The line of code$buildEventText = 'Powershell.exe $(SolutionDir)nupack.ps1 -projectdir $(ProjectDir) -config $(ConfigurationName) -outdir $(OutDir)'specifies the command that will be run by the post-build event. This is inserted directly into the project file's XML.
Getting at the information in the project file
Manipulating a VB.NET (or C#) project file using PowerShell is a bit more involved than one normally comes across.
The XML resides in its own custom namespace, so the usual PowerShell shortcut of $variable.tagname can't be used. Instead we have to resort to declaring a namespace in an associative array and then using this in our path expressions:
$ns = @{e='http://schemas.microsoft.com/developer/msbuild/2003'}
#$xml = [System.Xml.XmlDocument](get-content $proj.FullName);
$xml = New-Object System.Xml.XmlDocument
$xml.Load($proj.FullName)
$node = (select-xml -namespace $ns -xml $xml -XPath '//e:PostBuildEvent').node
You need to change the line
#constant definitions [string]$repositoryLocation = "\\servername\packagesharename"to point to the location where the packages will be stored as well.
This script is probably too verbose and a real PowerShell wizard might have pulled off the same trick in half the number of lines. Still, running this will instrument your entire solution to get it to package all its projects and copy them to the specified shared once you've built. You use it at your own risk: make a copy of your solution in a new folder structure before you let this loose on it.
I look forward to suggestions as to how this script might be improved.
No comments:
Post a Comment