hack/make.ps1
155435b6
 <#
 .NOTES
     Author:  @jhowardmsft
 
     Summary: Windows native build script. This is similar to functionality provided
              by hack\make.sh, but uses native Windows PowerShell semantics. It does
              not support the full set of options provided by the Linux counterpart.
              For example:
8d47858f
 
155435b6
              - You can't cross-build Linux docker binaries on Windows
              - Hashes aren't generated on binaries
              - 'Releasing' isn't supported.
              - Integration tests. This is because they currently cannot run inside a container,
8d47858f
                and require significant external setup.
 
              It does however provided the minimum necessary to support parts of local Windows
155435b6
              development and Windows to Windows CI.
 
8d47858f
              Usage Examples (run from repo root):
0a9c79f9
                 "hack\make.ps1 -Client" to build docker.exe client 64-bit binary (remote repo)
155435b6
                 "hack\make.ps1 -TestUnit" to run unit tests
0a9c79f9
                 "hack\make.ps1 -Daemon -TestUnit" to build the daemon and run unit tests
f32b267d
                 "hack\make.ps1 -All" to run everything this script knows about that can run in a container
0a9c79f9
                 "hack\make.ps1" to build the daemon binary (same as -Daemon)
                 "hack\make.ps1 -Binary" shortcut to -Client and -Daemon
155435b6
 
 .PARAMETER Client
      Builds the client binaries.
 
 .PARAMETER Daemon
      Builds the daemon binary.
 
 .PARAMETER Binary
0a9c79f9
      Builds the client and daemon binaries. A convenient shortcut to `make.ps1 -Client -Daemon`.
155435b6
 
 .PARAMETER Race
      Use -race in go build and go test.
 
 .PARAMETER Noisy
      Use -v in go build.
 
 .PARAMETER ForceBuildAll
      Use -a in go build.
 
 .PARAMETER NoOpt
      Use -gcflags -N -l in go build to disable optimisation (can aide debugging).
 
 .PARAMETER CommitSuffix
      Adds a custom string to be appended to the commit ID (spaces are stripped).
 
 .PARAMETER DCO
f32b267d
      Runs the DCO (Developer Certificate Of Origin) test (must be run outside a container).
155435b6
 
 .PARAMETER PkgImports
f32b267d
      Runs the pkg\ directory imports test (must be run outside a container).
155435b6
 
 .PARAMETER GoFormat
f32b267d
      Runs the Go formatting test (must be run outside a container).
155435b6
 
 .PARAMETER TestUnit
      Runs unit tests.
 
de45ce73
 .PARAMETER TestIntegration
      Runs integration tests.
 
155435b6
 .PARAMETER All
f32b267d
      Runs everything this script knows about that can run in a container.
155435b6
 
 
 TODO
 - Unify the head commit
 - Add golint and other checks (swagger maybe?)
 
 #>
 
 
 param(
     [Parameter(Mandatory=$False)][switch]$Client,
     [Parameter(Mandatory=$False)][switch]$Daemon,
     [Parameter(Mandatory=$False)][switch]$Binary,
     [Parameter(Mandatory=$False)][switch]$Race,
     [Parameter(Mandatory=$False)][switch]$Noisy,
     [Parameter(Mandatory=$False)][switch]$ForceBuildAll,
     [Parameter(Mandatory=$False)][switch]$NoOpt,
     [Parameter(Mandatory=$False)][string]$CommitSuffix="",
     [Parameter(Mandatory=$False)][switch]$DCO,
     [Parameter(Mandatory=$False)][switch]$PkgImports,
     [Parameter(Mandatory=$False)][switch]$GoFormat,
     [Parameter(Mandatory=$False)][switch]$TestUnit,
de45ce73
     [Parameter(Mandatory=$False)][switch]$TestIntegration,
155435b6
     [Parameter(Mandatory=$False)][switch]$All
 )
 
 $ErrorActionPreference = "Stop"
cef786f7
 $ProgressPreference = "SilentlyContinue"
155435b6
 $pushed=$False  # To restore the directory if we have temporarily pushed to one.
 
 # Utility function to get the commit ID of the repository
 Function Get-GitCommit() {
     if (-not (Test-Path ".\.git")) {
         # If we don't have a .git directory, but we do have the environment
         # variable DOCKER_GITCOMMIT set, that can override it.
         if ($env:DOCKER_GITCOMMIT.Length -eq 0) {
             Throw ".git directory missing and DOCKER_GITCOMMIT environment variable not specified."
         }
f48f1ff3
         Write-Host "INFO: Git commit ($env:DOCKER_GITCOMMIT) assumed from DOCKER_GITCOMMIT environment variable"
155435b6
         return $env:DOCKER_GITCOMMIT
     }
     $gitCommit=$(git rev-parse --short HEAD)
     if ($(git status --porcelain --untracked-files=no).Length -ne 0) {
         $gitCommit="$gitCommit-unsupported"
         Write-Host ""
         Write-Warning "This version is unsupported because there are uncommitted file(s)."
         Write-Warning "Either commit these changes, or add them to .gitignore."
         git status --porcelain --untracked-files=no | Write-Warning
         Write-Host ""
     }
     return $gitCommit
 }
 
 # Utility function to determine if we are running in a container or not.
 # In Windows, we get this through an environment variable set in `Dockerfile.Windows`
 Function Check-InContainer() {
     if ($env:FROM_DOCKERFILE.Length -eq 0) {
         Write-Host ""
         Write-Warning "Not running in a container. The result might be an incorrect build."
         Write-Host ""
5e7a9e52
         return $False
     }
     return $True
 }
 
 # Utility function to warn if the version of go is correct. Used for local builds
 # outside of a container where it may be out of date with master.
 Function Verify-GoVersion() {
     Try {
6fdd8371
         $goVersionDockerfile=(Select-String -Path ".\Dockerfile" -Pattern "^ARG[\s]+GO_VERSION=(.*)$").Matches.groups[1].Value -replace '\.0$',''
5e7a9e52
         $goVersionInstalled=(go version).ToString().Split(" ")[2].SubString(2)
     }
     Catch [Exception] {
         Throw "Failed to validate go version correctness: $_"
     }
     if (-not($goVersionInstalled -eq $goVersionDockerfile)) {
         Write-Host ""
         Write-Warning "Building with golang version $goVersionInstalled. You should update to $goVersionDockerfile"
         Write-Host ""
155435b6
     }
 }
 
 # Utility function to get the commit for HEAD
 Function Get-HeadCommit() {
     $head = Invoke-Expression "git rev-parse --verify HEAD"
     if ($LASTEXITCODE -ne 0) { Throw "Failed getting HEAD commit" }
 
     return $head
 }
 
 # Utility function to get the commit for upstream
 Function Get-UpstreamCommit() {
     Invoke-Expression "git fetch -q https://github.com/docker/docker.git refs/heads/master"
     if ($LASTEXITCODE -ne 0) { Throw "Failed fetching" }
 
     $upstream = Invoke-Expression "git rev-parse --verify FETCH_HEAD"
     if ($LASTEXITCODE -ne 0) { Throw "Failed getting upstream commit" }
 
     return $upstream
 }
 
 # Build a binary (client or daemon)
 Function Execute-Build($type, $additionalBuildTags, $directory) {
     # Generate the build flags
     $buildTags = "autogen"
     if ($Noisy)                     { $verboseParm=" -v" }
     if ($Race)                      { Write-Warning "Using race detector"; $raceParm=" -race"}
     if ($ForceBuildAll)             { $allParm=" -a" }
     if ($NoOpt)                     { $optParm=" -gcflags "+""""+"-N -l"+"""" }
39bcaee4
     if ($additionalBuildTags -ne "") { $buildTags += $(" " + $additionalBuildTags) }
155435b6
 
     # Do the go build in the appropriate directory
     # Note -linkmode=internal is required to be able to debug on Windows.
     # https://github.com/golang/go/issues/14319#issuecomment-189576638
     Write-Host "INFO: Building $type..."
     Push-Location $root\cmd\$directory; $global:pushed=$True
     $buildCommand = "go build" + `
                     $raceParm + `
                     $verboseParm + `
                     $allParm + `
                     $optParm + `
                     " -tags """ + $buildTags + """" + `
                     " -ldflags """ + "-linkmode=internal" + """" + `
                     " -o $root\bundles\"+$directory+".exe"
     Invoke-Expression $buildCommand
     if ($LASTEXITCODE -ne 0) { Throw "Failed to compile $type" }
     Pop-Location; $global:pushed=$False
 }
 
e538c1fd
 
155435b6
 # Validates the DCO marker is present on each commit
 Function Validate-DCO($headCommit, $upstreamCommit) {
     Write-Host "INFO: Validating Developer Certificate of Origin..."
     # Username may only contain alphanumeric characters or dashes and cannot begin with a dash
     $usernameRegex='[a-zA-Z0-9][a-zA-Z0-9-]+'
 
     $dcoPrefix="Signed-off-by:"
0ce6c12f
     $dcoRegex="^(Docker-DCO-1.1-)?$dcoPrefix ([^<]+) <([^<>@]+@[^<>]+)>( \(github: ($usernameRegex)\))?$"
155435b6
 
     $counts = Invoke-Expression "git diff --numstat $upstreamCommit...$headCommit"
     if ($LASTEXITCODE -ne 0) { Throw "Failed git diff --numstat" }
 
     # Counts of adds and deletes after removing multiple white spaces. AWK anyone? :(
e538c1fd
     $adds=0; $dels=0; $($counts -replace '\s+', ' ') | %{ 
         $a=$_.Split(" "); 
         if ($a[0] -ne "-") { $adds+=[int]$a[0] }
         if ($a[1] -ne "-") { $dels+=[int]$a[1] }
     }
     if (($adds -eq 0) -and ($dels -eq 0)) { 
155435b6
         Write-Warning "DCO validation - nothing to validate!"
         return
     }
 
     $commits = Invoke-Expression "git log  $upstreamCommit..$headCommit --format=format:%H%n"
     if ($LASTEXITCODE -ne 0) { Throw "Failed git log --format" }
     $commits = $($commits -split '\s+' -match '\S')
     $badCommits=@()
6130c89c
     $commits | ForEach-Object{
155435b6
         # Skip commits with no content such as merge commits etc
         if ($(git log -1 --format=format: --name-status $_).Length -gt 0) {
             # Ignore exit code on next call - always process regardless
             $commitMessage = Invoke-Expression "git log -1 --format=format:%B --name-status $_"
             if (($commitMessage -match $dcoRegex).Length -eq 0) { $badCommits+=$_ }
         }
     }
     if ($badCommits.Length -eq 0) {
         Write-Host "Congratulations!  All commits are properly signed with the DCO!"
     } else {
         $e = "`nThese commits do not have a proper '$dcoPrefix' marker:`n"
         $badCommits | %{ $e+=" - $_`n"}
         $e += "`nPlease amend each commit to include a properly formatted DCO marker.`n`n"
         $e += "Visit the following URL for information about the Docker DCO:`n"
         $e += "https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work`n"
         Throw $e
     }
 }
 
 # Validates that .\pkg\... is safely isolated from internal code
 Function Validate-PkgImports($headCommit, $upstreamCommit) {
     Write-Host "INFO: Validating pkg import isolation..."
 
     # Get a list of go source-code files which have changed under pkg\. Ignore exit code on next call - always process regardless
     $files=@(); $files = Invoke-Expression "git diff $upstreamCommit...$headCommit --diff-filter=ACMR --name-only -- `'pkg\*.go`'"
6130c89c
     $badFiles=@(); $files | ForEach-Object{
155435b6
         $file=$_
         # For the current changed file, get its list of dependencies, sorted and uniqued.
         $imports = Invoke-Expression "go list -e -f `'{{ .Deps }}`' $file"
         if ($LASTEXITCODE -ne 0) { Throw "Failed go list for dependencies on $file" }
8d47858f
         $imports = $imports -Replace "\[" -Replace "\]", "" -Split(" ") | Sort-Object | Get-Unique
155435b6
         # Filter out what we are looking for
ee508d47
         $imports = @() + $imports -NotMatch "^github.com/docker/docker/pkg/" `
                                   -NotMatch "^github.com/docker/docker/vendor" `
                                   -Match "^github.com/docker/docker" `
                                   -Replace "`n", ""
6130c89c
         $imports | ForEach-Object{ $badFiles+="$file imports $_`n" }
155435b6
     }
     if ($badFiles.Length -eq 0) {
         Write-Host 'Congratulations!  ".\pkg\*.go" is safely isolated from internal code.'
     } else {
         $e = "`nThese files import internal code: (either directly or indirectly)`n"
6130c89c
         $badFiles | ForEach-Object{ $e+=" - $_"}
155435b6
         Throw $e
     }
 }
 
 # Validates that changed files are correctly go-formatted
 Function Validate-GoFormat($headCommit, $upstreamCommit) {
     Write-Host "INFO: Validating go formatting on changed files..."
 
     # Verify gofmt is installed
     if ($(Get-Command gofmt -ErrorAction SilentlyContinue) -eq $nil) { Throw "gofmt does not appear to be installed" }
 
     # Get a list of all go source-code files which have changed.  Ignore exit code on next call - always process regardless
8d47858f
     $files=@(); $files = Invoke-Expression "git diff $upstreamCommit...$headCommit --diff-filter=ACMR --name-only -- `'*.go`'"
96e61f31
     $files = $files | Select-String -NotMatch "^vendor/"
155435b6
     $badFiles=@(); $files | %{
         # Deliberately ignore error on next line - treat as failed
8d47858f
         $content=Invoke-Expression "git show $headCommit`:$_"
155435b6
 
         # Next set of hoops are to ensure we have LF not CRLF semantics as otherwise gofmt on Windows will not succeed.
         # Also note that gofmt on Windows does not appear to support stdin piping correctly. Hence go through a temporary file.
         $content=$content -join "`n"
         $content+="`n"
         $outputFile=[System.IO.Path]::GetTempFileName()
         if (Test-Path $outputFile) { Remove-Item $outputFile }
         [System.IO.File]::WriteAllText($outputFile, $content, (New-Object System.Text.UTF8Encoding($False)))
a08e1304
         $currentFile = $_ -Replace("/","\")
         Write-Host Checking $currentFile
         Invoke-Expression "gofmt -s -l $outputFile"
         if ($LASTEXITCODE -ne 0) { $badFiles+=$currentFile }
155435b6
         if (Test-Path $outputFile) { Remove-Item $outputFile }
     }
     if ($badFiles.Length -eq 0) {
         Write-Host 'Congratulations!  All Go source files are properly formatted.'
     } else {
         $e = "`nThese files are not properly gofmt`'d:`n"
6130c89c
         $badFiles | ForEach-Object{ $e+=" - $_`n"}
155435b6
         $e+= "`nPlease reformat the above files using `"gofmt -s -w`" and commit the result."
         Throw $e
     }
 }
 
 # Run the unit tests
 Function Run-UnitTests() {
     Write-Host "INFO: Running unit tests..."
     $testPath="./..."
     $goListCommand = "go list -e -f '{{if ne .Name """ + '\"github.com/docker/docker\"' + """}}{{.ImportPath}}{{end}}' $testPath"
     $pkgList = $(Invoke-Expression $goListCommand)
     if ($LASTEXITCODE -ne 0) { Throw "go list for unit tests failed" }
     $pkgList = $pkgList | Select-String -Pattern "github.com/docker/docker"
     $pkgList = $pkgList | Select-String -NotMatch "github.com/docker/docker/vendor"
     $pkgList = $pkgList | Select-String -NotMatch "github.com/docker/docker/man"
e593b72c
     $pkgList = $pkgList | Select-String -NotMatch "github.com/docker/docker/integration"
155435b6
     $pkgList = $pkgList -replace "`r`n", " "
     $goTestCommand = "go test" + $raceParm + " -cover -ldflags -w -tags """ + "autogen daemon" + """ -a """ + "-test.timeout=10m" + """ $pkgList"
     Invoke-Expression $goTestCommand
     if ($LASTEXITCODE -ne 0) { Throw "Unit tests failed" }
 }
 
de45ce73
 # Run the integration tests
 Function Run-IntegrationTests() {
     $env:DOCKER_INTEGRATION_DAEMON_DEST = $root + "\bundles\tmp"
da9289fb
     $dirs = go list -test -f '{{- if ne .ForTest `"`" -}}{{- .Dir -}}{{- end -}}' .\integration\...
de45ce73
     $integration_api_dirs = @()
     ForEach($dir in $dirs) {
da9289fb
         $integration_api_dirs += $dir
         Write-Host "Building test suite binary $dir"
         go test -c -o "$dir\test.exe" $dir
de45ce73
     }
 
     ForEach($dir in $integration_api_dirs) {
da9289fb
         Set-Location $dir
de45ce73
         Write-Host "Running $($PWD.Path)"
         $pinfo = New-Object System.Diagnostics.ProcessStartInfo
         $pinfo.FileName = "$($PWD.Path)\test.exe"
791aa3c3
         $pinfo.WorkingDirectory = "$($PWD.Path)"
de45ce73
         $pinfo.RedirectStandardError = $true
         $pinfo.UseShellExecute = $false
         $pinfo.Arguments = $env:INTEGRATION_TESTFLAGS
         $p = New-Object System.Diagnostics.Process
         $p.StartInfo = $pinfo
         $p.Start() | Out-Null
         $p.WaitForExit()
         $err = $p.StandardError.ReadToEnd()
         if (($LASTEXITCODE -ne 0) -and ($err -notlike "*warning: no tests to run*")) {
             Throw "Integration tests failed: $err"
         }
     }
 }
 
155435b6
 # Start of main code.
 Try {
     Write-Host -ForegroundColor Cyan "INFO: make.ps1 starting at $(Get-Date)"
8c93a410
 
     # Get to the root of the repo
     $root = $(Split-Path $MyInvocation.MyCommand.Definition -Parent | Split-Path -Parent)
     Push-Location $root
155435b6
 
     # Handle the "-All" shortcut to turn on all things we can handle.
f32b267d
     # Note we expressly only include the items which can run in a container - the validations tests cannot
     # as they require the .git directory which is excluded from the image by .dockerignore
de45ce73
     if ($All) { $Client=$True; $Daemon=$True; $TestUnit=$True; }
155435b6
 
     # Handle the "-Binary" shortcut to build both client and daemon.
     if ($Binary) { $Client = $True; $Daemon = $True }
 
0a9c79f9
     # Default to building the daemon if not asked for anything explicitly.
de45ce73
     if (-not($Client) -and -not($Daemon) -and -not($DCO) -and -not($PkgImports) -and -not($GoFormat) -and -not($TestUnit) -and -not($TestIntegration)) { $Daemon=$True }
155435b6
 
     # Verify git is installed
     if ($(Get-Command git -ErrorAction SilentlyContinue) -eq $nil) { Throw "Git does not appear to be installed" }
 
     # Verify go is installed
     if ($(Get-Command go -ErrorAction SilentlyContinue) -eq $nil) { Throw "GoLang does not appear to be installed" }
 
     # Get the git commit. This will also verify if we are in a repo or not. Then add a custom string if supplied.
     $gitCommit=Get-GitCommit
     if ($CommitSuffix -ne "") { $gitCommit += "-"+$CommitSuffix -Replace ' ', '' }
 
47396d63
     # Get the version of docker (eg 17.04.0-dev)
1e1ad008
     $dockerVersion="0.0.0-dev"
edc639e9
     # Overwrite dockerVersion if VERSION Environment variable is available
     if (Test-Path Env:\VERSION) { $dockerVersion=$env:VERSION }
155435b6
 
8d47858f
     # Give a warning if we are not running in a container and are building binaries or running unit tests.
155435b6
     # Not relevant for validation tests as these are fine to run outside of a container.
5e7a9e52
     if ($Client -or $Daemon -or $TestUnit) { $inContainer=Check-InContainer }
 
     # If we are not in a container, validate the version of GO that is installed.
     if (-not $inContainer) { Verify-GoVersion }
155435b6
 
     # Verify GOPATH is set
     if ($env:GOPATH.Length -eq 0) { Throw "Missing GOPATH environment variable. See https://golang.org/doc/code.html#GOPATH" }
 
     # Run autogen if building binaries or running unit tests.
     if ($Client -or $Daemon -or $TestUnit) {
         Write-Host "INFO: Invoking autogen..."
195919d9
         Try { .\hack\make\.go-autogen.ps1 -CommitString $gitCommit -DockerVersion $dockerVersion -Platform "$env:PLATFORM" -Product "$env:PRODUCT" }
155435b6
         Catch [Exception] { Throw $_ }
     }
 
8d47858f
     # DCO, Package import and Go formatting tests.
155435b6
     if ($DCO -or $PkgImports -or $GoFormat) {
         # We need the head and upstream commits for these
         $headCommit=Get-HeadCommit
         $upstreamCommit=Get-UpstreamCommit
 
         # Run DCO validation
         if ($DCO) { Validate-DCO $headCommit $upstreamCommit }
 
         # Run `gofmt` validation
         if ($GoFormat) { Validate-GoFormat $headCommit $upstreamCommit }
 
         # Run pkg isolation validation
         if ($PkgImports) { Validate-PkgImports $headCommit $upstreamCommit }
     }
 
     # Build the binaries
     if ($Client -or $Daemon) {
         # Create the bundles directory if it doesn't exist
         if (-not (Test-Path ".\bundles")) { New-Item ".\bundles" -ItemType Directory | Out-Null }
 
         # Perform the actual build
         if ($Daemon) { Execute-Build "daemon" "daemon" "dockerd" }
32915b1d
         if ($Client) {
cef786f7
             # Get the Docker channel and version from the environment, or use the defaults.
468eb93e
             if (-not ($channel = $env:DOCKERCLI_CHANNEL)) { $channel = "stable" }
             if (-not ($version = $env:DOCKERCLI_VERSION)) { $version = "17.06.2-ce" }
cef786f7
 
             # Download the zip file and extract the client executable.
             Write-Host "INFO: Downloading docker/cli version $version from $channel..."
             $url = "https://download.docker.com/win/static/$channel/x86_64/docker-$version.zip"
             Invoke-WebRequest $url -OutFile "docker.zip"
32d47be2
             Try {
cef786f7
                 Add-Type -AssemblyName System.IO.Compression.FileSystem
                 $zip = [System.IO.Compression.ZipFile]::OpenRead("$PWD\docker.zip")
                 Try {
                     if (-not ($entry = $zip.Entries | Where-Object { $_.Name -eq "docker.exe" })) {
                         Throw "Cannot find docker.exe in $url"
                     }
                     [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, "$PWD\bundles\docker.exe", $true)
                 }
                 Finally {
                     $zip.Dispose()
                 }
32d47be2
             }
             Finally {
cef786f7
                 Remove-Item -Force "docker.zip"
32d47be2
             }
32915b1d
         }
155435b6
     }
 
     # Run unit tests
     if ($TestUnit) { Run-UnitTests }
 
de45ce73
     # Run integration tests
     if ($TestIntegration) { Run-IntegrationTests }
 
155435b6
     # Gratuitous ASCII art.
     if ($Daemon -or $Client) {
         Write-Host
         Write-Host -ForegroundColor Green " ________   ____  __."
         Write-Host -ForegroundColor Green " \_____  \ `|    `|/ _`|"
         Write-Host -ForegroundColor Green " /   `|   \`|      `<"
         Write-Host -ForegroundColor Green " /    `|    \    `|  \"
         Write-Host -ForegroundColor Green " \_______  /____`|__ \"
         Write-Host -ForegroundColor Green "         \/        \/"
         Write-Host
     }
 }
 Catch [Exception] {
     Write-Host -ForegroundColor Red ("`nERROR: make.ps1 failed:`n$_")
d2788cb2
     Write-Host -ForegroundColor Red ($_.InvocationInfo.PositionMessage)
155435b6
 
     # More gratuitous ASCII art.
     Write-Host
     Write-Host -ForegroundColor Red  "___________      .__.__             .___"
     Write-Host -ForegroundColor Red  "\_   _____/____  `|__`|  `|   ____   __`| _/"
     Write-Host -ForegroundColor Red  " `|    __) \__  \ `|  `|  `| _/ __ \ / __ `| "
     Write-Host -ForegroundColor Red  " `|     \   / __ \`|  `|  `|_\  ___// /_/ `| "
     Write-Host -ForegroundColor Red  " \___  /  (____  /__`|____/\___  `>____ `| "
     Write-Host -ForegroundColor Red  "     \/        \/             \/     \/ "
     Write-Host
8c22a00b
 
     Throw $_
155435b6
 }
 Finally {
8c93a410
     Pop-Location # As we pushed to the root of the repo as the very first thing
155435b6
     if ($global:pushed) { Pop-Location }
     Write-Host -ForegroundColor Cyan "INFO: make.ps1 ended at $(Get-Date)"
 }