Recently, one of my build scripts failed with an odd error, claiming that the application wasn’t recognized. Given that the script had been working fine the previous day, this error was rather confusing.

Exception: The term 
'C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.10\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.11\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.2\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.6\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.8\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.9\tools\ReportGenerator.exe' 
is not recognized as the name of a cmdlet, function, script file, or operable 
program. Check the spelling of the name, or if a path was included, verify that 
the path is correct and try again.

Investigating the problem, I soon discovered the cause.

NuGet used to always store downloaded packages into a project specific .\packages\ folder. This folder only ever contained a single version of any one package, and my build scripts had previously only ever found one match when resolving wildcards.

This has all changed.

To avoid downloading the same package over and over again, and perhaps to save disk space too, NuGet now uses a cache of packages stored under the user profile. This means that there may now legitimately be multiple versions of a single package, catering for different projects needing different versions.

My build script failed because resolve-path call returned multiple results instead of just one.

PS> $packagesFolder = $env:userprofile\.nuget\packages
PS> resolve-path $packagesFolder\reportgenerator\*\tools\ReportGenerator.exe

Path
----
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.10\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.11\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.2\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.6\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.8\tools\ReportGenerator.exe
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.9\tools\ReportGenerator.exe

How do we select just one version to use?

My initial solution was something like this:

PS> $packagesFolder = $env:userprofile\.nuget\packages
PS> resolve-path $packagesFolder\reportgenerator\*\tools\ReportGenerator.exe | 
        sort-object | select-object -last 1
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.9\tools\ReportGenerator.exe

Unfortunately, this didn’t return the version I expected - the result was version 2.5.9, not 2.5.11.

What we need is a natural sort, one that respects numbers, not an alphabetical sort. Fortunately, a natural sort is fairly easy to do in PowerShell, by using a regular expression to expand all the numbers to a fixed width:

PS> $packagesFolder = $env:userprofile\.nuget\packages
PS> $toNatural = { [regex]::Replace($_, '\d+', { $args[0].Value.PadLeft(20) }) }
PS> resolve-path $packagesFolder\reportgenerator\*\tools\ReportGenerator.exe | 
        sort-object $toNatural | select-object -last 1
C:\Users\Bevan\.nuget\packages\reportgenerator\2.5.11\tools\ReportGenerator.exe

The variable $toNatural contains a PowerShell block, used obtain the sort key for each item as they are sorted.

The revised task to find ReportGenerator:

Task Requires.ReportGenerator {

    $packagesFolder = env:userprofile\.nuget\packages
    $toNatural = { [regex]::Replace($_, '\d+', { $args[0].Value.PadLeft(20) }) }
    $script:reportGeneratorExe =
        resolve-path $packagesFolder\reportgenerator\*\tools\ReportGenerator.exe | 
            sort-object $toNatural | select-object -last 1

    if ($reportGeneratorExe -eq $null)
    {
        throw "Failed to find ReportGenerator.exe"
    }

    Write-Host "Found Report Generator here: $reportGeneratorExe"
}

Now that this particular problem has been solved, I’m applying the same fix to all my other Require.* tasks so that I don’t get caught out in future.

Prior post in this series:
NuGet and .NET Core
Next post in this series:
Tracking time

Comments

blog comments powered by Disqus