Using NuGet for Sitecore dev instances (Part 2)

Last week I started writing up the content from my Sitecore Technical User Group presentation on using NuGet for the easy creation of new development instances. This week I’m continuing that topic, with the next steps for package creation:

Adding the rest of the files

The next step is to take the web.config file that we didn’t move last week, and move it into the “Content” folder of the package project. Then you can add XDT files to update the web.config when the target project gets run. The key settings you need to update are the various folders that are configured:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <sc.variable xdt:Transform="SetAttributes" xdt:Locator="Match(name)"  name="dataFolder" value="./data"/>
    <sc.variable xdt:Transform="SetAttributes" xdt:Locator="Match(name)" name="mediaFolder" value="./upload"/>
    <sc.variable xdt:Transform="SetAttributes" xdt:Locator="Match(name)" name="tempFolder" value="./temp"/>
  </sitecore>  
</configuration>

These paths need to be made relative, as these folders are are going to live under our website folder.

Next, you have an option about where you want to put your license file. You can either create a “Data” folder under “Content” and put the license file there, or you can put the license file into the “Data” folder we created earlier under the “SitecoreFiles” folder.

Then you need to create an “App_Config” folder under the “Content” folder to put your config changes into. The first config file you want to put here is the connection strings file for your site. In order to prevent SQL Server from complaining about the naming of the database files when we attach them, we need to set the names of the databases in the connection string. NuGet provides a convenient way to do this as it can pre-process files to do token replacement when the file is deployed from your package to the target project.

To do this, you need to rename the standard connection strings file to be ConnectionStrings.config.pp and then update its content a bit:

<?xml version="1.0" encoding="utf-8"?>
<connectionStrings>
  <add name="core" connectionString="Data Source=(LocalDB)\v11.0;Integrated Security=true;AttachDbFilename=|DataDirectory|\Sitecore.Core.mdf;Database=$rootnamespace$_Core" />
  <add name="master" connectionString="Data Source=(LocalDB)\v11.0;Integrated Security=true;AttachDbFilename=|DataDirectory|\Sitecore.Master.mdf;Database=$rootnamespace$_Master" />
  <add name="web" connectionString="Data Source=(LocalDB)\v11.0;Integrated Security=true;AttachDbFilename=|DataDirectory|\Sitecore.Web.mdf;Database=$rootnamespace$_Web" />
</connectionStrings>

For each connection string the file needs to set the data source as LocalDB so that we can attach it automatically at runtime. It sets the path for the file to the “|DataDirectory|” macro. Then finally it uses the “$rootnamespace$” token so that NuGet will replace this with the root namespace of whatever project you install the package into.

At this point you can also add an “Include” folder under “App_Config” and add any extra configuration patches you require.

Adding the PowerShell script

The final task for package creation is to add the PowerShell script we mentioned briefly in part 1. I ended up with three script files in my “Tools”folder:

First script: A PowerShell module file containing some reusable functions

This file can be named whatever you like, but mine is called “Sitecore-Package.psm1”. It’s a place to put any reusable functions that the rest of the scripts refer to:

The first function is a recursive copy operation that will transfer files from the “SitecoreFiles” folder (under the package folder) into the target website project’s folder. Rather than being a simple copy, the code checks to see if any of the files already exists in the project and skips them if they do. This allows files in the target web project to override files in the package. This is important if you customise parts of Sitecore inside your project.

The code for this function is:

function Copy-PackageFiles-Recursive($projectItem, $folder, $sourceFolder, $destFolder, $level)
{
  Write-Host $level "RecursiveCopy projectItem: " $projectItem.Name " - " $folder

  Get-ChildItem $folder | Foreach {
    if( (Get-Item $_.FullName) -is [System.IO.DirectoryInfo] )
    {
      Write-Host $level "Folder: " $_.FullName

      $path = ($_.FullName + "\") -Replace [Regex]::Escape($sourceFolder), $destFolder
      if( !(Test-Path $Path) )
      {
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
      }

      $newProjectItem = $null 
      try
      {
        $newProjectItem = $projectItem.ProjectItems.Item($_.Name)
      }
      catch
      {
      }

      Copy-PackageFiles-Recursive $newProjectItem $_.FullName $sourceFolder $destFolder ($level+"-")
    }
    else
    {
      Write-Host $level "File: " $_.Name

      $item = $null
      try
      {
        $item = $projectItem.ProjectItems.Item($_.Name)
        Write-Host $level "Project contains this file - skipping: " $_.Name
      }
      catch
      {
      }

      if( $item -eq $null )
      {
        Write-Host $level "Project does not contain this file: " $_.Name
        $path = $_.FullName -Replace [Regex]::Escape($sourceFolder), $destFolder

        if(Test-Path $path)
        {
          Write-Host $level "Disk already contains file - skipping: " $_.Name
        }
        else
        {
          Write-Host $level "Copying: " $_.Name
          Copy-Item $_.FullName -Destination $path -Force
        }
      }
    }
  }
}

Since this has come from some exploratory work, there’s a fair bit of debug output here to echo all the behaviour out to the console. But generally, it’s a recursive function which processes all the child items of each folder. For the folders it checks to see if they exist at the target location (and creates it if not) and creates it if necessary. For files it only copies items which do not exist at the target.

The second function in this file is a script “commandlet” for PowerShell, which will later be added to Visual Studio’s package manager console. It’s job is to run the copy function above:

function Refresh-Sitecore($packageName = "TestPackage") {
  Write-Host "Refreshing Sitecore files in web project..."

  $project = Get-Project
  $projectFolder = Split-Path -parent $project.FullName
  $dest = $projectFolder

  $pkg = Get-Package $packageName
  $relativePath = "..\packages\" + $pkg.Id + "." + $pkg.Version
  $installPath = Join-Path -path $projectFolder -childPath $relativePath
  $installPath = [System.IO.Path]::GetFullPath( $installPath )

  $source = Join-Path -path $installPath -childPath "SitecoreFiles"

  Write-host "-- Project Name: $($project.Name)"
  Write-host "-- Source path for package: $source"
  Write-host "-- Dest path web project: $dest"

  Write-Host "--> Performing copy of non-project files"
  Copy-PackageFiles-Recursive $project $source $source $dest "-"
}

This calculates the correct source and target folder paths, and calls the recursive copy operation. Again there’s a bit of debug output in there which you might not choose to keep in a non-exploratory version.

Having this available in the Visual Studio Package Manager window will mean that we can re-execute the recursive copy if necessary. The key use-case for this is if you do a get-latest on a website project you’ve not used before. Source Control will fetch the web project and its NuGet package definitions. The NuGet Package Restore behaviour can then re-download the package files to ensure they’re available – but this won’t re-run the install/initialisation scripts that we’ll mention in a bit. So you need a way to re-copy the Sitecore files – and this commandlet is the way. You run it passing the name of the web project you want to copy the files to.

The final thing you need in this script file is to ensure that these two functions are public, so they can be reused by other files:

Export-ModuleMember Refresh-Sitecore
Export-ModuleMember Copy-PackageFiles-Recursive

Second script: The install script called when the package is added to a project

This script needs to be named “install.ps1” so that NuGet can find and run it at when your package is added to the web project. The code it contains is:

param($installPath, $toolsPath, $package, $project)

Write-Host "install.ps1 executing for " $project.FullName

$projectFolder = Split-Path -parent $project.FullName
$source = Join-Path -path $installPath -childPath "SitecoreFiles"
$dest = $projectFolder

Write-Host "--> Performing copy of non-project files"
Copy-PackageFiles-Recursive $project $source $source $dest "-"

Write-Host "--> Making web.debug.config dependent on web.config"
$dbg = Join-Path -path $projectFolder -childPath "Web.Debug.config"
$project.ProjectItems.Item("Web.config").ProjectItems.AddFromFile($dbg)

Write-Host "--> Making web.release.config dependent on web.config"
$rel = Join-Path -path $projectFolder -childPath "Web.Release.config"
$project.ProjectItems.Item("Web.config").ProjectItems.AddFromFile($rel)

Write-Host "--> Creating missing folders"

$ovr = Join-Path -path $projectFolder -childPath "sitecore\shell\override"
if(-Not( Test-Path $ovr ))
{
  New-Item $ovr -type directory
}

$mod = Join-Path -path $projectFolder -childPath "sitecore modules"
if(-Not( Test-Path $mod ))
{
  New-Item $mod -type directory
}

Write-Host "install.ps1 Done."

Similarly to what we saw in the commandlet above, this calculates the source and target folders, and runs the recursive copy operation.

It then runs a couple of operations against the API for the target project’s files to ensure that the XDT files we added become “children” of the web.config in the project tree.

And finally it checks whether a couple of important folders (that may not exist) are there or not, and creates them if required.

Third script: The initialisation script called when the package is first downloaded

The final file is a simple script to register the commandlet above with the Visual Studio Package Manager window. This needs to be called “init.ps1”.

param($installPath, $toolsPath, $package)

Import-Module (Join-Path $toolsPath Sitecore-Package.psm1) -Force -DisableNameChecking

We use the “DisableNameChecking” flag here to avoid a warning from PowerShell due to the verb name I chose for my commandlet. You could name it something less contentious if you wanted to avoid needing this flag.

Build and publish

The next step is to add all the files described so far into the package project. In your package project, turn on “Show All Files” and add all the files under each of the folders we created earlier:

Add Package Files

This is easy enough for “Content”, “Tools” and “Lib” as they are small, but it will take a while under “SitecoreFiles” due to the number of files that need adding. I find my copy of Visual Studio becomes unresponsive while doing that last step, but it recovers itself fine once it’s finished going through all the files.

Once this is done you can run a build, and once it completes the job of generating the manifest data for NuGet and zipping everything up, you’ll end up with a package file under the output directory of your project. And you can publish that to your private package feed ready for developers to consume.

Next week I’ll look at some conclusions drawn from testing out this process.

Advertisements

One thought on “Using NuGet for Sitecore dev instances (Part 2)

  1. Pingback: Using NuGet for Sitecore dev instances (Part 3) | Jeremy Davis

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s