Tizen_4.0 base
[platform/upstream/docker-engine.git] / hack / make.ps1
1 <#
2 .NOTES
3     Author:  @jhowardmsft
4
5     Summary: Windows native build script. This is similar to functionality provided
6              by hack\make.sh, but uses native Windows PowerShell semantics. It does
7              not support the full set of options provided by the Linux counterpart.
8              For example:
9
10              - You can't cross-build Linux docker binaries on Windows
11              - Hashes aren't generated on binaries
12              - 'Releasing' isn't supported.
13              - Integration tests. This is because they currently cannot run inside a container,
14                and require significant external setup.
15
16              It does however provided the minimum necessary to support parts of local Windows
17              development and Windows to Windows CI.
18
19              Usage Examples (run from repo root):
20                 "hack\make.ps1 -Client" to build docker.exe client 64-bit binary (remote repo)
21                 "hack\make.ps1 -TestUnit" to run unit tests
22                 "hack\make.ps1 -Daemon -TestUnit" to build the daemon and run unit tests
23                 "hack\make.ps1 -All" to run everything this script knows about that can run in a container
24                 "hack\make.ps1" to build the daemon binary (same as -Daemon)
25                 "hack\make.ps1 -Binary" shortcut to -Client and -Daemon
26
27 .PARAMETER Client
28      Builds the client binaries.
29
30 .PARAMETER Daemon
31      Builds the daemon binary.
32
33 .PARAMETER Binary
34      Builds the client and daemon binaries. A convenient shortcut to `make.ps1 -Client -Daemon`.
35
36 .PARAMETER Race
37      Use -race in go build and go test.
38
39 .PARAMETER Noisy
40      Use -v in go build.
41
42 .PARAMETER ForceBuildAll
43      Use -a in go build.
44
45 .PARAMETER NoOpt
46      Use -gcflags -N -l in go build to disable optimisation (can aide debugging).
47
48 .PARAMETER CommitSuffix
49      Adds a custom string to be appended to the commit ID (spaces are stripped).
50
51 .PARAMETER DCO
52      Runs the DCO (Developer Certificate Of Origin) test (must be run outside a container).
53
54 .PARAMETER PkgImports
55      Runs the pkg\ directory imports test (must be run outside a container).
56
57 .PARAMETER GoFormat
58      Runs the Go formatting test (must be run outside a container).
59
60 .PARAMETER TestUnit
61      Runs unit tests.
62
63 .PARAMETER All
64      Runs everything this script knows about that can run in a container.
65
66
67 TODO
68 - Unify the head commit
69 - Add golint and other checks (swagger maybe?)
70
71 #>
72
73
74 param(
75     [Parameter(Mandatory=$False)][switch]$Client,
76     [Parameter(Mandatory=$False)][switch]$Daemon,
77     [Parameter(Mandatory=$False)][switch]$Binary,
78     [Parameter(Mandatory=$False)][switch]$Race,
79     [Parameter(Mandatory=$False)][switch]$Noisy,
80     [Parameter(Mandatory=$False)][switch]$ForceBuildAll,
81     [Parameter(Mandatory=$False)][switch]$NoOpt,
82     [Parameter(Mandatory=$False)][string]$CommitSuffix="",
83     [Parameter(Mandatory=$False)][switch]$DCO,
84     [Parameter(Mandatory=$False)][switch]$PkgImports,
85     [Parameter(Mandatory=$False)][switch]$GoFormat,
86     [Parameter(Mandatory=$False)][switch]$TestUnit,
87     [Parameter(Mandatory=$False)][switch]$All
88 )
89
90 $ErrorActionPreference = "Stop"
91 $pushed=$False  # To restore the directory if we have temporarily pushed to one.
92
93 # Utility function to get the commit ID of the repository
94 Function Get-GitCommit() {
95     if (-not (Test-Path ".\.git")) {
96         # If we don't have a .git directory, but we do have the environment
97         # variable DOCKER_GITCOMMIT set, that can override it.
98         if ($env:DOCKER_GITCOMMIT.Length -eq 0) {
99             Throw ".git directory missing and DOCKER_GITCOMMIT environment variable not specified."
100         }
101         Write-Host "INFO: Git commit ($env:DOCKER_GITCOMMIT) assumed from DOCKER_GITCOMMIT environment variable"
102         return $env:DOCKER_GITCOMMIT
103     }
104     $gitCommit=$(git rev-parse --short HEAD)
105     if ($(git status --porcelain --untracked-files=no).Length -ne 0) {
106         $gitCommit="$gitCommit-unsupported"
107         Write-Host ""
108         Write-Warning "This version is unsupported because there are uncommitted file(s)."
109         Write-Warning "Either commit these changes, or add them to .gitignore."
110         git status --porcelain --untracked-files=no | Write-Warning
111         Write-Host ""
112     }
113     return $gitCommit
114 }
115
116 # Utility function to get get the current build version of docker
117 Function Get-DockerVersion() {
118     if (-not (Test-Path ".\VERSION")) { Throw "VERSION file not found. Is this running from the root of a docker repository?" }
119     return $(Get-Content ".\VERSION" -raw).ToString().Replace("`n","").Trim()
120 }
121
122 # Utility function to determine if we are running in a container or not.
123 # In Windows, we get this through an environment variable set in `Dockerfile.Windows`
124 Function Check-InContainer() {
125     if ($env:FROM_DOCKERFILE.Length -eq 0) {
126         Write-Host ""
127         Write-Warning "Not running in a container. The result might be an incorrect build."
128         Write-Host ""
129         return $False
130     }
131     return $True
132 }
133
134 # Utility function to warn if the version of go is correct. Used for local builds
135 # outside of a container where it may be out of date with master.
136 Function Verify-GoVersion() {
137     Try {
138         $goVersionDockerfile=(Get-Content ".\Dockerfile" | Select-String "ENV GO_VERSION").ToString().Split(" ")[2]
139         $goVersionInstalled=(go version).ToString().Split(" ")[2].SubString(2)
140     }
141     Catch [Exception] {
142         Throw "Failed to validate go version correctness: $_"
143     }
144     if (-not($goVersionInstalled -eq $goVersionDockerfile)) {
145         Write-Host ""
146         Write-Warning "Building with golang version $goVersionInstalled. You should update to $goVersionDockerfile"
147         Write-Host ""
148     }
149 }
150
151 # Utility function to get the commit for HEAD
152 Function Get-HeadCommit() {
153     $head = Invoke-Expression "git rev-parse --verify HEAD"
154     if ($LASTEXITCODE -ne 0) { Throw "Failed getting HEAD commit" }
155
156     return $head
157 }
158
159 # Utility function to get the commit for upstream
160 Function Get-UpstreamCommit() {
161     Invoke-Expression "git fetch -q https://github.com/docker/docker.git refs/heads/master"
162     if ($LASTEXITCODE -ne 0) { Throw "Failed fetching" }
163
164     $upstream = Invoke-Expression "git rev-parse --verify FETCH_HEAD"
165     if ($LASTEXITCODE -ne 0) { Throw "Failed getting upstream commit" }
166
167     return $upstream
168 }
169
170 # Build a binary (client or daemon)
171 Function Execute-Build($type, $additionalBuildTags, $directory) {
172     # Generate the build flags
173     $buildTags = "autogen"
174     if ($Noisy)                     { $verboseParm=" -v" }
175     if ($Race)                      { Write-Warning "Using race detector"; $raceParm=" -race"}
176     if ($ForceBuildAll)             { $allParm=" -a" }
177     if ($NoOpt)                     { $optParm=" -gcflags "+""""+"-N -l"+"""" }
178     if ($addtionalBuildTags -ne "") { $buildTags += $(" " + $additionalBuildTags) }
179
180     # Do the go build in the appropriate directory
181     # Note -linkmode=internal is required to be able to debug on Windows.
182     # https://github.com/golang/go/issues/14319#issuecomment-189576638
183     Write-Host "INFO: Building $type..."
184     Push-Location $root\cmd\$directory; $global:pushed=$True
185     $buildCommand = "go build" + `
186                     $raceParm + `
187                     $verboseParm + `
188                     $allParm + `
189                     $optParm + `
190                     " -tags """ + $buildTags + """" + `
191                     " -ldflags """ + "-linkmode=internal" + """" + `
192                     " -o $root\bundles\"+$directory+".exe"
193     Invoke-Expression $buildCommand
194     if ($LASTEXITCODE -ne 0) { Throw "Failed to compile $type" }
195     Pop-Location; $global:pushed=$False
196 }
197
198
199 # Validates the DCO marker is present on each commit
200 Function Validate-DCO($headCommit, $upstreamCommit) {
201     Write-Host "INFO: Validating Developer Certificate of Origin..."
202     # Username may only contain alphanumeric characters or dashes and cannot begin with a dash
203     $usernameRegex='[a-zA-Z0-9][a-zA-Z0-9-]+'
204
205     $dcoPrefix="Signed-off-by:"
206     $dcoRegex="^(Docker-DCO-1.1-)?$dcoPrefix ([^<]+) <([^<>@]+@[^<>]+)>( \(github: ($usernameRegex)\))?$"
207
208     $counts = Invoke-Expression "git diff --numstat $upstreamCommit...$headCommit"
209     if ($LASTEXITCODE -ne 0) { Throw "Failed git diff --numstat" }
210
211     # Counts of adds and deletes after removing multiple white spaces. AWK anyone? :(
212     $adds=0; $dels=0; $($counts -replace '\s+', ' ') | %{ 
213         $a=$_.Split(" "); 
214         if ($a[0] -ne "-") { $adds+=[int]$a[0] }
215         if ($a[1] -ne "-") { $dels+=[int]$a[1] }
216     }
217     if (($adds -eq 0) -and ($dels -eq 0)) { 
218         Write-Warning "DCO validation - nothing to validate!"
219         return
220     }
221
222     $commits = Invoke-Expression "git log  $upstreamCommit..$headCommit --format=format:%H%n"
223     if ($LASTEXITCODE -ne 0) { Throw "Failed git log --format" }
224     $commits = $($commits -split '\s+' -match '\S')
225     $badCommits=@()
226     $commits | %{
227         # Skip commits with no content such as merge commits etc
228         if ($(git log -1 --format=format: --name-status $_).Length -gt 0) {
229             # Ignore exit code on next call - always process regardless
230             $commitMessage = Invoke-Expression "git log -1 --format=format:%B --name-status $_"
231             if (($commitMessage -match $dcoRegex).Length -eq 0) { $badCommits+=$_ }
232         }
233     }
234     if ($badCommits.Length -eq 0) {
235         Write-Host "Congratulations!  All commits are properly signed with the DCO!"
236     } else {
237         $e = "`nThese commits do not have a proper '$dcoPrefix' marker:`n"
238         $badCommits | %{ $e+=" - $_`n"}
239         $e += "`nPlease amend each commit to include a properly formatted DCO marker.`n`n"
240         $e += "Visit the following URL for information about the Docker DCO:`n"
241         $e += "https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work`n"
242         Throw $e
243     }
244 }
245
246 # Validates that .\pkg\... is safely isolated from internal code
247 Function Validate-PkgImports($headCommit, $upstreamCommit) {
248     Write-Host "INFO: Validating pkg import isolation..."
249
250     # Get a list of go source-code files which have changed under pkg\. Ignore exit code on next call - always process regardless
251     $files=@(); $files = Invoke-Expression "git diff $upstreamCommit...$headCommit --diff-filter=ACMR --name-only -- `'pkg\*.go`'"
252     $badFiles=@(); $files | %{
253         $file=$_
254         # For the current changed file, get its list of dependencies, sorted and uniqued.
255         $imports = Invoke-Expression "go list -e -f `'{{ .Deps }}`' $file"
256         if ($LASTEXITCODE -ne 0) { Throw "Failed go list for dependencies on $file" }
257         $imports = $imports -Replace "\[" -Replace "\]", "" -Split(" ") | Sort-Object | Get-Unique
258         # Filter out what we are looking for
259         $imports = $imports -NotMatch "^github.com/docker/docker/pkg/" `
260                             -NotMatch "^github.com/docker/docker/vendor" `
261                             -Match "^github.com/docker/docker" `
262                             -Replace "`n", ""
263         $imports | % { $badFiles+="$file imports $_`n" }
264     }
265     if ($badFiles.Length -eq 0) {
266         Write-Host 'Congratulations!  ".\pkg\*.go" is safely isolated from internal code.'
267     } else {
268         $e = "`nThese files import internal code: (either directly or indirectly)`n"
269         $badFiles | %{ $e+=" - $_"}
270         Throw $e
271     }
272 }
273
274 # Validates that changed files are correctly go-formatted
275 Function Validate-GoFormat($headCommit, $upstreamCommit) {
276     Write-Host "INFO: Validating go formatting on changed files..."
277
278     # Verify gofmt is installed
279     if ($(Get-Command gofmt -ErrorAction SilentlyContinue) -eq $nil) { Throw "gofmt does not appear to be installed" }
280
281     # Get a list of all go source-code files which have changed.  Ignore exit code on next call - always process regardless
282     $files=@(); $files = Invoke-Expression "git diff $upstreamCommit...$headCommit --diff-filter=ACMR --name-only -- `'*.go`'"
283     $files = $files | Select-String -NotMatch "^vendor/"
284     $badFiles=@(); $files | %{
285         # Deliberately ignore error on next line - treat as failed
286         $content=Invoke-Expression "git show $headCommit`:$_"
287
288         # Next set of hoops are to ensure we have LF not CRLF semantics as otherwise gofmt on Windows will not succeed.
289         # Also note that gofmt on Windows does not appear to support stdin piping correctly. Hence go through a temporary file.
290         $content=$content -join "`n"
291         $content+="`n"
292         $outputFile=[System.IO.Path]::GetTempFileName()
293         if (Test-Path $outputFile) { Remove-Item $outputFile }
294         [System.IO.File]::WriteAllText($outputFile, $content, (New-Object System.Text.UTF8Encoding($False)))
295         $currentFile = $_ -Replace("/","\")
296         Write-Host Checking $currentFile
297         Invoke-Expression "gofmt -s -l $outputFile"
298         if ($LASTEXITCODE -ne 0) { $badFiles+=$currentFile }
299         if (Test-Path $outputFile) { Remove-Item $outputFile }
300     }
301     if ($badFiles.Length -eq 0) {
302         Write-Host 'Congratulations!  All Go source files are properly formatted.'
303     } else {
304         $e = "`nThese files are not properly gofmt`'d:`n"
305         $badFiles | %{ $e+=" - $_`n"}
306         $e+= "`nPlease reformat the above files using `"gofmt -s -w`" and commit the result."
307         Throw $e
308     }
309 }
310
311 # Run the unit tests
312 Function Run-UnitTests() {
313     Write-Host "INFO: Running unit tests..."
314     $testPath="./..."
315     $goListCommand = "go list -e -f '{{if ne .Name """ + '\"github.com/docker/docker\"' + """}}{{.ImportPath}}{{end}}' $testPath"
316     $pkgList = $(Invoke-Expression $goListCommand)
317     if ($LASTEXITCODE -ne 0) { Throw "go list for unit tests failed" }
318     $pkgList = $pkgList | Select-String -Pattern "github.com/docker/docker"
319     $pkgList = $pkgList | Select-String -NotMatch "github.com/docker/docker/vendor"
320     $pkgList = $pkgList | Select-String -NotMatch "github.com/docker/docker/man"
321     $pkgList = $pkgList | Select-String -NotMatch "github.com/docker/docker/integration-cli"
322     $pkgList = $pkgList -replace "`r`n", " "
323     $goTestCommand = "go test" + $raceParm + " -cover -ldflags -w -tags """ + "autogen daemon" + """ -a """ + "-test.timeout=10m" + """ $pkgList"
324     Invoke-Expression $goTestCommand
325     if ($LASTEXITCODE -ne 0) { Throw "Unit tests failed" }
326 }
327
328 # Start of main code.
329 Try {
330     Write-Host -ForegroundColor Cyan "INFO: make.ps1 starting at $(Get-Date)"
331
332     # Get to the root of the repo
333     $root = $(Split-Path $MyInvocation.MyCommand.Definition -Parent | Split-Path -Parent)
334     Push-Location $root
335
336     # Handle the "-All" shortcut to turn on all things we can handle.
337     # Note we expressly only include the items which can run in a container - the validations tests cannot
338     # as they require the .git directory which is excluded from the image by .dockerignore
339     if ($All) { $Client=$True; $Daemon=$True; $TestUnit=$True }
340
341     # Handle the "-Binary" shortcut to build both client and daemon.
342     if ($Binary) { $Client = $True; $Daemon = $True }
343
344     # Default to building the daemon if not asked for anything explicitly.
345     if (-not($Client) -and -not($Daemon) -and -not($DCO) -and -not($PkgImports) -and -not($GoFormat) -and -not($TestUnit)) { $Daemon=$True }
346
347     # Verify git is installed
348     if ($(Get-Command git -ErrorAction SilentlyContinue) -eq $nil) { Throw "Git does not appear to be installed" }
349
350     # Verify go is installed
351     if ($(Get-Command go -ErrorAction SilentlyContinue) -eq $nil) { Throw "GoLang does not appear to be installed" }
352
353     # Get the git commit. This will also verify if we are in a repo or not. Then add a custom string if supplied.
354     $gitCommit=Get-GitCommit
355     if ($CommitSuffix -ne "") { $gitCommit += "-"+$CommitSuffix -Replace ' ', '' }
356
357     # Get the version of docker (eg 17.04.0-dev)
358     $dockerVersion=Get-DockerVersion
359
360     # Give a warning if we are not running in a container and are building binaries or running unit tests.
361     # Not relevant for validation tests as these are fine to run outside of a container.
362     if ($Client -or $Daemon -or $TestUnit) { $inContainer=Check-InContainer }
363
364     # If we are not in a container, validate the version of GO that is installed.
365     if (-not $inContainer) { Verify-GoVersion }
366
367     # Verify GOPATH is set
368     if ($env:GOPATH.Length -eq 0) { Throw "Missing GOPATH environment variable. See https://golang.org/doc/code.html#GOPATH" }
369
370     # Run autogen if building binaries or running unit tests.
371     if ($Client -or $Daemon -or $TestUnit) {
372         Write-Host "INFO: Invoking autogen..."
373         Try { .\hack\make\.go-autogen.ps1 -CommitString $gitCommit -DockerVersion $dockerVersion }
374         Catch [Exception] { Throw $_ }
375     }
376
377     # DCO, Package import and Go formatting tests.
378     if ($DCO -or $PkgImports -or $GoFormat) {
379         # We need the head and upstream commits for these
380         $headCommit=Get-HeadCommit
381         $upstreamCommit=Get-UpstreamCommit
382
383         # Run DCO validation
384         if ($DCO) { Validate-DCO $headCommit $upstreamCommit }
385
386         # Run `gofmt` validation
387         if ($GoFormat) { Validate-GoFormat $headCommit $upstreamCommit }
388
389         # Run pkg isolation validation
390         if ($PkgImports) { Validate-PkgImports $headCommit $upstreamCommit }
391     }
392
393     # Build the binaries
394     if ($Client -or $Daemon) {
395         # Create the bundles directory if it doesn't exist
396         if (-not (Test-Path ".\bundles")) { New-Item ".\bundles" -ItemType Directory | Out-Null }
397
398         # Perform the actual build
399         if ($Daemon) { Execute-Build "daemon" "daemon" "dockerd" }
400         if ($Client) {
401             # Get the repo and commit of the client to build.
402             "hack\dockerfile\binaries-commits" | ForEach-Object {
403                 $dockerCliRepo = ((Get-Content $_ | Select-String "DOCKERCLI_REPO") -split "=")[1]
404                 $dockerCliCommit = ((Get-Content $_ | Select-String "DOCKERCLI_COMMIT") -split "=")[1]
405             }
406
407             # Build from a temporary directory.
408             $tempLocation = "$env:TEMP\$(New-Guid)"
409             New-Item -ItemType Directory $tempLocation | Out-Null
410
411             # Temporarily override GOPATH, then clone, checkout, and build.
412             $saveGOPATH = $env:GOPATH
413             Try {
414                 $env:GOPATH = $tempLocation
415                 $dockerCliRoot = "$env:GOPATH\src\github.com\docker\cli"
416                 Write-Host "INFO: Cloning client repository..."
417                 Invoke-Expression "git clone -q $dockerCliRepo $dockerCliRoot"
418                 if ($LASTEXITCODE -ne 0) { Throw "Failed to clone client repository $dockerCliRepo" }
419                 Invoke-Expression "git -C $dockerCliRoot  checkout -q $dockerCliCommit"
420                 if ($LASTEXITCODE -ne 0) { Throw "Failed to checkout client commit $dockerCliCommit" }
421                 Write-Host "INFO: Building client..."
422                 Push-Location "$dockerCliRoot\cmd\docker"; $global:pushed=$True
423                 Invoke-Expression "go build -o $root\bundles\docker.exe"
424                 if ($LASTEXITCODE -ne 0) { Throw "Failed to compile client" }
425                 Pop-Location; $global:pushed=$False
426             }
427             Catch [Exception] {
428                 Throw $_
429             }
430             Finally {
431                 # Always restore GOPATH and remove the temporary directory.
432                 $env:GOPATH = $saveGOPATH
433                 Remove-Item -Force -Recurse $tempLocation
434             }
435         }
436     }
437
438     # Run unit tests
439     if ($TestUnit) { Run-UnitTests }
440
441     # Gratuitous ASCII art.
442     if ($Daemon -or $Client) {
443         Write-Host
444         Write-Host -ForegroundColor Green " ________   ____  __."
445         Write-Host -ForegroundColor Green " \_____  \ `|    `|/ _`|"
446         Write-Host -ForegroundColor Green " /   `|   \`|      `<"
447         Write-Host -ForegroundColor Green " /    `|    \    `|  \"
448         Write-Host -ForegroundColor Green " \_______  /____`|__ \"
449         Write-Host -ForegroundColor Green "         \/        \/"
450         Write-Host
451     }
452 }
453 Catch [Exception] {
454     Write-Host -ForegroundColor Red ("`nERROR: make.ps1 failed:`n$_")
455
456     # More gratuitous ASCII art.
457     Write-Host
458     Write-Host -ForegroundColor Red  "___________      .__.__             .___"
459     Write-Host -ForegroundColor Red  "\_   _____/____  `|__`|  `|   ____   __`| _/"
460     Write-Host -ForegroundColor Red  " `|    __) \__  \ `|  `|  `| _/ __ \ / __ `| "
461     Write-Host -ForegroundColor Red  " `|     \   / __ \`|  `|  `|_\  ___// /_/ `| "
462     Write-Host -ForegroundColor Red  " \___  /  (____  /__`|____/\___  `>____ `| "
463     Write-Host -ForegroundColor Red  "     \/        \/             \/     \/ "
464     Write-Host
465
466     Throw $_
467 }
468 Finally {
469     Pop-Location # As we pushed to the root of the repo as the very first thing
470     if ($global:pushed) { Pop-Location }
471     Write-Host -ForegroundColor Cyan "INFO: make.ps1 ended at $(Get-Date)"
472 }