BuildUtils.psm1

<#
.SYNOPSIS
Get location of latest msbuild installed on the system
 
.DESCRIPTION
It requires modules VSSetup installed in the system, you can install
with standard instruction
 
Install-Module VSSetup -Scope CurrentUser -Force
 
.EXAMPLE
 
$msbuildLocation = Get-LatestMsbuildLocation
set-alias msb $msbuildLocation
 
msb yoursolution.sln
#>

function Get-LatestMsbuildLocation
{
  Param 
  (
    [bool] $allowPreviewVersions = $false
  )
    if ($allowPreviewVersions) {
      $latestVsInstallationInfo = Get-VSSetupInstance -All -Prerelease | Sort-Object -Property InstallationVersion -Descending | Select-Object -First 1
    } else {
      $latestVsInstallationInfo = Get-VSSetupInstance -All | Sort-Object -Property InstallationVersion -Descending | Select-Object -First 1
    }
    Write-Debug "Latest version installed is $($latestVsInstallationInfo.InstallationVersion)"
    if ($latestVsInstallationInfo.InstallationVersion -like "15.*") {
      $msbuildLocation = "$($latestVsInstallationInfo.InstallationPath)\MSBuild\15.0\Bin\msbuild.exe"
    
      Write-Debug "Located msbuild for Visual Studio 2017 in $msbuildLocation"
    } else {
      $msbuildLocation = "$($latestVsInstallationInfo.InstallationPath)\MSBuild\Current\Bin\msbuild.exe"
      Write-Debug "Located msbuild in $msbuildLocation"
    }

    return $msbuildLocation
}
<#
.SYNOPSIS
Get location of latest nuget in temp folder, if nuget.exe is not found it will download
 
.DESCRIPTION
 
.EXAMPLE
 
$nugetLocation = Get-NugetLocation
set-alias nuget $nugetLocation
 
nuget yoursolution.sln
#>

function Get-NugetLocation
{
  Param 
  (
  )
    $nugetLocation = "$env:TEMP\nuget.exe"
    if (!(Test-Path -Path $nugetLocation)) {

      Invoke-WebRequest -Uri "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile $nugetLocation
    }

    return $nugetLocation
}


<#
.SYNOPSIS
Manipulate all assemblyinfo.vb and assemblyinfo.cs to change version
 
.DESCRIPTION
With legacy .NET project versioning is done with specific attribtues
inside assemblyinfo.cs and assemblyinfo.vb files. This cmdlet
can scan all files and perform a substitution.
 
This should be done before compiling (and usually after version is
determined with tools like gitversion)
 
This is a perfect match for Invoke-Getversion function
 
.PARAMETER SrcPath
Source path
 
.PARAMETER assemblyVersion
AssemblyVersionAttribute
 
.PARAMETER fileAssemblyVersion
FileAssemblyVersionAttribute
 
.PARAMETER assemblyInformationalVersion
AssemblyinformationalVersion
 
.EXAMPLE
 
$version = Invoke-GitVersion
Update-SourceVersion -SrcPath PathWithSource -assemblyVersion $version.assemblyVersion -fileAssemblyVersion $version.assemblyFileVersion -assemblyInformationalVersion = $version.assemblyInformationalVersion
 
#>

function Update-SourceVersion
{
  Param 
  (
    [string]$SrcPath,
    [string]$assemblyVersion, 
    [string]$fileAssemblyVersion,
    [string]$assemblyInformationalVersion,
    [bool]$modifyAssemblyVersion = $true
  )
    
    if ($fileAssemblyVersion -eq "")
    {
        $fileAssemblyVersion = $assemblyVersion
    }

        
    if ($assemblyInformationalVersion -eq "")
    {
        $assemblyInformationalVersion = $fileAssemblyVersion
    }
    
    Write-Debug "Executing Update-SourceVersion in path $SrcPath, Version is $assemblyVersion and File Version is $fileAssemblyVersion and Informational Version is $assemblyInformationalVersion"
        
    $AllVersionFiles = Get-ChildItem $SrcPath\* -Include AssemblyInfo.cs,AssemblyInfo.vb -recurse
  
    foreach ($file in $AllVersionFiles)
    { 
      Write-Debug "Modifying file $($file.FullName)"
      #save the file for restore
      $backFile = $file.FullName + "._ORI"

      Copy-Item $file.FullName $backFile -Force
      #now load all content of the original file and rewrite modified to the same file
      $content = Get-Content $file.FullName 
      Remove-Item $file.FullName
      if ($modifyAssemblyVersion) 
      {
        $content |
          %{$_ -replace 'AssemblyVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', "AssemblyVersion(""$assemblyVersion"")" } |
          %{$_ -replace 'AssemblyFileVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', "AssemblyFileVersion(""$fileAssemblyVersion"")" } |
          %{$_ -replace 'AssemblyInformationalVersion\(".*"\)', "AssemblyInformationalVersion(""$assemblyInformationalVersion"")" } |
          Set-Content -Encoding UTF8 -Path $file.FullName -Force
      }
      else {
        $content |
          %{$_ -replace 'AssemblyFileVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', "AssemblyFileVersion(""$fileAssemblyVersion"")" } |
          %{$_ -replace 'AssemblyInformationalVersion\(".*"\)', "AssemblyInformationalVersion(""$assemblyInformationalVersion"")" } |
          Set-Content -Encoding UTF8 -Path $file.FullName -Force
      }
    }
}

<#
.SYNOPSIS
Simply allow for easy XML node manipulation in context of classic .NET xml configuration
files. The purpose is to substitute specific part of the configuration file with
predetermined tokens.
 
.DESCRIPTION
Basically takes an XML object and then perform a lookup of a specific part of the XML with
standard XPATH notation and perform a substitution.
 
.EXAMPLE
 
$configFile = "somedirecory/web.config"
$xml = [xml](Get-Content $configFile)
Edit-XmlNodes $xml -xpath "/configuration/appSettings/add[@key='apiClientSecret']/@value" -value "__APICLIENTSECRET__"
$xml.save($configFile)
 
.NOTES
 
#>

function Edit-XmlNodes {
    param (
        [xml] $doc = $(throw "doc is a required parameter"),
        [string] $xpath = $(throw "xpath is a required parameter"),
        [string] $value = $(throw "value is a required parameter"),
        [bool] $condition = $true
    )    
        if ($condition -eq $true) {
            $nodes = $doc.SelectNodes($xpath)
             
            foreach ($node in $nodes) {
                if ($node -ne $null) {
                    if ($node.NodeType -eq "Element") {
                        $node.InnerXml = $value
                    }
                    else {
                        $node.Value = $value
                    }
                }
            }
        }
    }
  

<#
.SYNOPSIS
Unzip a zip file using standard .NET framework classes.
 
.PARAMETER zipFile
Path of zip file
 
.PARAMETER destinationFolder
Destination folder where to uncompress files
 
.PARAMETER deleteOld
If $true the routine will delete original file.
 
.PARAMETER quietMode
if $true it will output less information on output stream
#>

function Expand-WithFramework(
    [string] $zipFile,
    [string] $destinationFolder,
    [bool] $deleteOld = $false,
    [bool] $quietMode = $false
)
{
    Add-Type -AssemblyName System.IO.Compression.FileSystem
    if ((Test-Path $destinationFolder) -and $deleteOld)
    {
          Remove-Item $destinationFolder -Recurse -Force
    }
    [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $destinationFolder)
}

<#
.SYNOPSIS
Get location of 7za.exe and download if not present
 
.DESCRIPTION
Get location of 7zip executable (7za) in temp directory,
if the executable is not present it will download and
save on disk. It will return the location of 7za executable
that can be in turn used to compress / uncompress files and dir.
 
.EXAMPLE
 
Suppose that we want to compress $source directory in a file
called $Target
 
$sevenZipExe = Get-7ZipLocation
set-alias sz $sevenZipExe
 
Write-Output "Zipping folder $Source in file $Target"
sz a -mx=9 -r -mmt=on $Target $Source
 
.NOTES
 
#>

function Get-7ZipLocation()
{
    $exeLocation = "$env:TEMP\7z"
    $sevenZipExe = "$exeLocation\7za.exe"
    Write-Debug "Testing for 7zip executable [$sevenZipExe]"
    if (-not (test-path $sevenZipExe)) 
    {
        Write-Debug "7zip executable [$sevenZipExe] not present, download from http://www.7-zip.org/a/7za920.zip to $env:TEMP\7zip.zip"
        Invoke-WebRequest -Uri "http://www.7-zip.org/a/7za920.zip" -OutFile "$env:TEMP\7zip.zip"
        Write-Debug "Unzipping $env:TEMP\7zip.zip to directory $exeLocation"
        Expand-WithFramework -zipFile "$env:TEMP\7zip.zip" -destinationFolder "$exeLocation" -quietMode $true 
        $sevenZipExe = "$exeLocation\7za.exe"
    } 
    return $sevenZipExe 
}
<#
.SYNOPSIS
Check if last command was successful and then print error and take correct action
 
.DESCRIPTION
It uses $? variable to check if last command was successful and then call
Write-LogError cmdlet to log an error and make CI execution fail if we are im
Ci
 
.EXAMPLE
 
dotnet build foo.sln
Assert-LastExecution -message "Failed to foo the bar." -haltExecution $true
 
.NOTES
if you are in Continuous Integratino script the cmdlet expects
to have variable $ci_engine set to one of the supported value = ["azdo", "github"]
 
#>

function Assert-LastExecution(
    [string] $message,
    [bool] $haltExecution = $false) 
{
    if ($false -eq $?)
    {
        Write-LogError -Message $message -HaltExecution $haltExecution
    }
}
<#
.SYNOPSIS
Allow for asking a simple yes/no question
 
.DESCRIPTION
Given a question this function ask for a yes/no response
it handles default value, casing and accepts both y n short answer
or long yes no version
 
.EXAMPLE
 
Get-YesNoAnswer -question "Do you want to do this?" -default $true
 
.NOTES
 
#>

function Get-YesNoAnswer(
    [string] $question,
    [System.Nullable[bool]] $default = $null) 
{
  $yesValues = "y", "yes";
  $noValues = "n", "no";
  do 
  {
    $realQuestion = $question.TrimEnd(":");
    if ($default -eq $true) 
    {
      $realQuestion += ": (Y/n)" 
    }
    elseif ($default -eq $true) 
    {
      $realQuestion += ": (y/N)" 
    }
    else 
    {
      $realQuestion += ": (y/n)" 
    }
    Write-Host $realQuestion -NoNewline
    $answer = Read-Host
    if ($answer -eq '' -and $default -ne $null) 
    {
        return $default
    }
    $answer = $answer.ToLower()
  } while (!$yesValues.Contains($answer) -and !$noValues.Contains($answer))

  return $yesValues.Contains($answer);
}

<#
.SYNOPSIS
Write a log error CI enabled
 
.DESCRIPTION
Simply write a log error to the console and write also a special
command for continous integration engine to have the script signal
an error in the step. Remember to set $ci_engine to the correct value
for your CI engine
 
.EXAMPLE
 
Write-LogError -message "Failed to foo the bar." -$haltExecution $true
 
.NOTES
$ci_engine valid values are ["azdo", "github"]
 
#>

function Write-LogError(
    [string] $message,
    [bool] $haltExecution = $false) 
{
    # Trying to detect if we are running inside a CI engine, to try to emit correct error message
    if ("azdo" -eq $ci_engine)
    {
        Write-Host "##vso[task.logissue type=error]$message"
    }
    elseif ("github" -eq $ci_engine)
    {
        Write-Host "::error::$message"
    }
    
    # Write with Write-error always.
    Write-Host $message -ForegroundColor Red

    if ($true -eq $haltExecution)
    {
        Write-Host "Request script exit!!!!!"
        exit (1)
    }
}
<#
.SYNOPSIS
Get / download nunit test runners and return the folder
with the console runner
 
.DESCRIPTION
 
.EXAMPLE
 
$nunitConsoleRunner = GEt-NunitTestsConsoleRunner
set-alias nunit "$nunitConsoleRunner\nunit3-console-exe"
 
#>

function Get-NunitTestsConsoleRunner
{
  Param 
  (
  )
    
    $nunitLocation = "$env:TEMP\nunitrunners"
    $consoleRunner = ""
    if (Test-Path -Path $nunitLocation) {
      $consoleRunner = Get-ChildItem -Path $nunitLocation -Name nunit3-console.exe -Recurse 
      if ($consoleRunner -eq $null) 
      {
        Write-Debug "no runner found in folder $nunitLocation"
        Remove-Item $nunitLocation -Recurse
      }
    }

    if (!(Test-Path -Path $nunitLocation)) {
      $nugetLocation = Get-NugetLocation
      set-alias nugetinternal $nugetLocation
      nugetinternal install NUnit.Runners -OutputDirectory $nunitLocation | Out-Null
    }

    #Now we need to locate the console runner
    $consoleRunner = Get-ChildItem -Path $nunitLocation -Name nunit3-console.exe -Recurse 
    Write-Debug "Found nunit runner in $nunitLocation\$consoleRunner"
    return "$nunitLocation\$consoleRunner"
}
<#
.SYNOPSIS
It contains all the data needed to work with gitversion as well as a success
property that returns to the caller if the invocation of GitVersion succeeded
or failed.
#>

class GitVersion 
{
  [bool] $Success
  [string]$AssemblyVersion
  [string]$AssemblyFileVersion
  [string]$NugetVersion
  [string]$AssemblyInformationalVersion
  [string]$FullSemver
}

<#
.SYNOPSIS
Execute GitVersion using dotnet core version of the tool
 
.DESCRIPTION
This command requires that gitversion tool was already configured in
.config folder of the project in standard dotnet-tools.json file. A simple
content could be
 
{
  "version": 1,
  "isRoot": true,
  "tools": {
    "gitversion.tool": {
      "version": "5.2.4",
      "commands": [
        "dotnet-gitversion"
      ]
    }
  }
}
 
.PARAMETER ConfigurationFile
Location of GitVersion.yml file, you can specify full path to the file
 
.EXAMPLE
 
$version = Invoke-GitVersion
 
if ($version.Success) #You can check for success of operation.
 
Write-Host "Assembly version is $($version.AssemblyVersion)"
Write-Host "File version is $($version.AssemblyFileVersion)"
Write-Host "Nuget version is $($version.NugetVersion)"
Write-Host "Informational version is $($version.AssemblyInformationalVersion)"
#>

function Invoke-Gitversion
{
  [OutputType([GitVersion])]
  Param 
  (
      [string] $ConfigurationFile = "GitVersion.yml"
  )

  Write-Information -MessageData "Running Invoke-Gitversion to determine version numbers for current repository."

  $sampleContent = '{
  "version": 1,
  "isRoot": true,
  "tools": {
    "gitversion.tool": {
      "version": "5.2.4",
      "commands": [
        "dotnet-gitversion"
      ]
    }
  }
}'


  $sampleGitVersion = 'branches: {}
ignore:
  sha: []
merge-message-formats: {}
mode: ContinuousDeployment'


  $retvalue = [GitVersion]::new()

  Write-Verbose "Checking present of dotnet-tools.json file"
  $toolFile = "./.config/dotnet-tools.json"
  $configFile = "./.config/GitVersion.yml"
  if (-not (Test-Path "./.config"))
  {
    New-Item -ItemType Directory -Path ".config"
  }
  if (-not (Test-Path $toolFile))
  {
    Write-Debug "Config file $toolFile does not exists will create one"
    Set-Content -Path $toolFile -Value $sampleContent
  }
  if (-not (Test-Path $configFile))
  {
    Write-Debug "Config file $configFile does not exists will create one"
    Set-Content -Path $configFile -Value $sampleGitVersion
  }

  Write-Verbose "restoring tooling for gitversion"
  dotnet tool restore | Out-Null

  if ($false -eq $?) 
  {
    Write-Error "Unable to run dotnet tool restore, execution of the command failed"
    $retvalue.Success = $false
    return $retvalue
  }

  Write-Verbose "Running gitversion to determine version with config file $ConfigurationFile"
  $gitVersionOutput = dotnet tool run dotnet-gitversion /config $ConfigurationFile | Out-String
  if ($false -eq $?) 
  {
    Write-Error "Unable to run dotnet tool run dotnet-gitversion, execution of the command failed: $gitVersionOutput"
    $retvalue.Success = $false
    return $retvalue
  }

  Write-Verbose "Raw GitVersion output"
  Write-Verbose $gitVersionOutput

  $version = $gitVersionOutput | Out-String | ConvertFrom-Json

  Write-Verbose "Parsed value to be returned"
  $retvalue.Success = $true
  $retvalue.AssemblyVersion = $version.AssemblySemVer
  $retvalue.AssemblyFileVersion = $version.AssemblySemFileVer
  $retvalue.NuGetVersion = $version.NuGetVersionV2
  $retvalue.AssemblyInformationalVersion = $version.FullSemVer + "." + $version.Sha
  $retvalue.FullSemVer = $version.FullSemVer

  Write-Verbose "Assembly version is $($retvalue.AssemblyVersion)"
  Write-Verbose "File version is $($retvalue.AssemblyFileVersion)"
  Write-Verbose "Nuget version is $($retvalue.NuGetVersionV2)"
  Write-Verbose "Informational version is $($retvalue.AssemblyInformationalVersion)"
  Write-Verbose "FullSemVer version is $($retvalue.FullSemVer)"

  return $retvalue
}
Export-ModuleMember -Function * -Cmdlet *