Skip to main content

Useful PowerShell Visual Studio project build utilities found in the SonarLint extension for VisualStudio repository on Github.

Add-Type -AssemblyName "System.IO.Compression.FileSystem"

$sonarqube_runner_version = "3.0.0.629"

# Resolves the given relative to the repository path to absolute.
function Resolve-RepoPath([string]$relativePath) {
    return (Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..")) $relativePath)
}

# Original: http://jameskovacs.com/2010/02/25/the-exec-problem
function Exec ([scriptblock]$command, [string]$errorMessage = "Error executing command: " + $command) {
    $output = & $command
    if ((-not $?) -or ($lastexitcode -ne 0)) {
        Write-Host $output
        throw $errorMessage
    }
    return $output
}

function Test-ExitCode([string]$errorMessage = "Error executing command.") {
    if ((-not $?) -or ($lastexitcode -ne 0)) {
        throw $errorMessage
    }
}

# Sets the current folder and executes the given script.
# When the script finishes sets the original current folder.
function Exec-InLocation([string]$path, [scriptblock]$command) {
    try {
        Push-Location $path
        & $command
    }
    finally {
        Pop-Location
    }
}

function Write-Header([string]$text) {
    Write-Host "================================================"
    Write-Host $text
    Write-Host "================================================"
}

## Build ############################################################

function Get-BuildNumber([string]$default = "0") {
    if ($env:BUILD_NUMBER) {
        return $env:BUILD_NUMBER
    }
    return $default
}

function Get-BranchName {
    if ($env:GITHUB_BRANCH) {
        if ($env:GITHUB_BRANCH.StartsWith("refs/heads/")) {
            return $env:GITHUB_BRANCH.Substring(11)
        }
        return $env:GITHUB_BRANCH
    }
    return Exec { & git rev-parse --abbrev-ref HEAD }
}

function Get-Sha1 {
    if ($env:GIT_SHA1) {
        return $env:GIT_SHA1
    }
    return Exec { & git rev-parse HEAD }
}

function Get-ExecutablePath([string]$name, [string]$directory, [string]$envVar) {
    $path = [environment]::GetEnvironmentVariable($envVar, "Process")

    if (!$path) {
        if (!$directory) {
            $path = Exec { & where.exe $name } `
                | Select-Object -First 1
        } else {
            $path = Exec { & where.exe /R $directory $name } `
                | Select-Object -First 1
        }
    }

    if (Test-Path $path) {
        Write-Host "Found ${name} at ${path}"
        [environment]::SetEnvironmentVariable($envVar, $path)
        return $path
    }

    Write-Error "Cannot find ${name} in ${path}."
    exit 1
}

function Get-NuGetPath {
    return Get-ExecutablePath -name "nuget.exe" -envVar "NUGET_PATH"
}

function Get-MsBuildPath {
    return Get-ExecutablePath -name "msbuild.exe" -envVar "MSBUILD_PATH"
}

function Get-VsTestPath {
    return Get-ExecutablePath -name "VSTest.Console.exe" -envVar "VSTEST_PATH"
}

function Get-CodeCoveragePath {
    $vstest_exe = Get-VsTestPath
    $codeCoverageDirectory = Join-Path (Get-ChildItem $vstest_exe).Directory "..\..\..\..\.."
    return Get-ExecutablePath -name "CodeCoverage.exe" -directory $codeCoverageDirectory -envVar "CODE_COVERAGE_PATH"
}

function Expand-ZIPFile($source, $destination) {
    Write-Host "Expanding ZIP file ${source}"
    $application = New-Object -Com Shell.Application
    $zip = $application.NameSpace($source)
    foreach ($item in $zip.items()) {
        $application.NameSpace($destination).CopyHere($item, 0x14)
    }
}

function Get-SonarQubeRunnerPath {
    $sonarqube_runner_exe = (Resolve-RepoPath "MSBuild.SonarQube.Runner.exe")

    if (Test-Path $sonarqube_runner_exe) {
        return $sonarqube_runner_exe
    }

    $downloadLink = "https://github.com/SonarSource/sonar-msbuild-runner/releases/download/${sonarqube_runner_version}/sonar-scanner-msbuild-${sonarqube_runner_version}.zip"
    $sonarqube_runner_zip = (Resolve-RepoPath "MSBuild.SonarQube.Runner.zip")

    # NB: the WebClient class defaults to TLS v1, which is no longer supported by GitHub
    # See https://githubengineering.com/crypto-removal-notice/
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
    Write-Debug "Current security protocol: $([System.Net.ServicePointManager]::SecurityProtocol)"
    Write-Host "Attempting to download Scanner for MSBuild from ${downloadLink}"
    (New-Object System.Net.WebClient).DownloadFile($downloadLink, $sonarqube_runner_zip)

    # perhaps we could use other folder, not the repository root
    Expand-ZIPFile $sonarqube_runner_zip (Resolve-RepoPath "")
    # PS v5.0 -> Expand-Archive $sonarqube_runner_zip (Resolve-RepoPath "") -Force

    Remove-Item $sonarqube_runner_zip -Force

    Write-Debug "Found MSBuild.SonarQube.Runner.exe at ${sonarqube_runner_exe}"

    return $sonarqube_runner_exe
}

function Set-Version {
    Write-Header "Updating version in all files..."

    $buildNumber = Get-BuildNumber
    $branchName = Get-BranchName
    $sha1 = Get-Sha1

    Write-Host "Setting build number ${buildNumber}, sha1 ${sha1} and branch ${branchName}"

    $versionPropsPath = (Resolve-RepoPath "build\Version.props")

    (Get-Content $versionPropsPath) `
 -Replace '<Sha1>.*</Sha1>', "<Sha1>$sha1</Sha1>" `
 -Replace '<BuildNumber>\d+</BuildNumber>', "<BuildNumber>$buildNumber</BuildNumber>" `
 -Replace '<BranchName>.*</BranchName>', "<BranchName>$branchName</BranchName>" `
        | Set-Content $versionPropsPath

    $msbuild_exe = Get-MsBuildPath
    $changeVersionProj = (Resolve-RepoPath build\ChangeVersion.proj)
    Exec { & $msbuild_exe $changeVersionProj }

    $version = Get-Version
    Write-Host "Version successfully set to '${version}'"
}

function Get-Version {
    [xml]$versionProps = Get-Content (Resolve-RepoPath ".\build\Version.props")
    return $versionProps.Project.PropertyGroup.MainVersion + "." + $versionProps.Project.PropertyGroup.BuildNumber
}

function Restore-Packages ([string]$solutionPath) {
    Write-Header "Restoring NuGet packages..."
    $nuget_exe = Get-NuGetPath
    & $nuget_exe restore $solutionPath
    Test-ExitCode "ERROR: Restoring NuGet packages FAILED."
}

function Begin-Analysis(
    [string]$sonarQubeUrl,
    [string]$sonarQubeToken,
    [string]$sonarQubeProjectKey,
    [string]$sonarQubeProjectName,
    [array][parameter(ValueFromRemainingArguments = $true)] $remainingArgs) {

    Write-Header "Running SonarQube Analysis begin step..."

    $sonarqube_runner_exe = Get-SonarQubeRunnerPath

    & $sonarqube_runner_exe begin `
        /k:$sonarQubeProjectKey `
        /n:$sonarQubeProjectName `
        /d:sonar.host.url=$sonarQubeUrl `
        /d:sonar.login=$sonarQubeToken `
        $remainingArgs
    Test-ExitCode "ERROR: SonarQube Analysis begin step FAILED."
}

function End-Analysis([string]$sonarQubeToken) {
    Write-Header "Running SonarQube Analysis end step..."

    $sonarqube_runner_exe = Get-SonarQubeRunnerPath

    & $sonarqube_runner_exe end /d:sonar.login=$sonarQubeToken
    Test-ExitCode "ERROR: SonarQube Analysis end step FAILED."
}

function Build-Solution (
    [string][Parameter(Mandatory = $true, Position = 0)]$solutionPath,
    [array][parameter(ValueFromRemainingArguments = $true)] $remainingArgs) {

    Write-Header "Building solution ${solutionPath}..."

    Restore-Packages $solutionPath

    $msbuild_exe = Get-MsBuildPath

    & $msbuild_exe $solutionPath $remainingArgs
    Test-ExitCode "ERROR: Build FAILED."
}

function Build-ReleaseSolution(
    [string][Parameter(Mandatory = $true, Position = 0)]$solutionPath,
    [string][Parameter(Mandatory = $true, Position = 1)]$certificatePath,
    [array][parameter(ValueFromRemainingArguments = $true)] $remainingArgs) {

    Build-Solution $solutionPath `
        /v:m `
        /p:configuration=Release `
        /p:DeployExtension=false `
        /p:ZipPackageCompressionLevel=normal `
        /p:defineConstants=SignAssembly `
        /p:SignAssembly=true `
        /p:AssemblyOriginatorKeyFile=$certificatePath `
        $remainingArgs
}

function Run-Tests([bool]$runCoverage=$false) {
    Write-Header "Starting test execution..."
    Write-Host "Is running code coverage: ${runCoverage}"

    $testFiles = @()
    Get-ChildItem (Resolve-RepoPath "src") -Recurse -Include @("*.UnitTests.dll", "*.Tests.dll") `
        | Where-Object { $_.DirectoryName -Match "bin" } `
        | ForEach-Object { $testFiles += $_ }

    Write-Host "Running unit tests for: ${testFiles}"

    $vstest_exe = Get-VsTestPath
    & $vstest_exe $testFiles /InIsolation /Enablecodecoverage /UseVsixExtensions:true /Logger:trx /Diag:vstest.log
    Test-ExitCode "ERROR: Unit Tests execution FAILED."

    if ($runCoverage) {
        $codeCoverage_exe = Get-CodeCoveragePath
        Write-Host "Generating code coverage reports:"
        Get-ChildItem (Resolve-RepoPath "") -Recurse -Include "*.coverage" | ForEach-Object {
            $filePathWithNewExtension = $_.FullName + "xml"
            Write-Host "  ${filePathWithNewExtension}"
            & $codeCoverage_exe analyze /output:$filePathWithNewExtension $_.FullName
            Test-ExitCode "ERROR: Code coverage reports generation FAILED."
        }
    }
}