428e8c9607a6402ef462b7ffcfd52deb597937a5
[platform/upstream/coreclr.git] / eng / common / post-build / sourcelink-validation.ps1
1 param(
2   [Parameter(Mandatory=$true)][string] $InputPath,              # Full path to directory where Symbols.NuGet packages to be checked are stored
3   [Parameter(Mandatory=$true)][string] $ExtractPath,            # Full path to directory where the packages will be extracted during validation
4   [Parameter(Mandatory=$false)][string] $GHRepoName,            # GitHub name of the repo including the Org. E.g., dotnet/arcade
5   [Parameter(Mandatory=$false)][string] $GHCommit,              # GitHub commit SHA used to build the packages
6   [Parameter(Mandatory=$true)][string] $SourcelinkCliVersion    # Version of SourceLink CLI to use
7 )
8
9 . $PSScriptRoot\post-build-utils.ps1
10
11 # Cache/HashMap (File -> Exist flag) used to consult whether a file exist 
12 # in the repository at a specific commit point. This is populated by inserting
13 # all files present in the repo at a specific commit point.
14 $global:RepoFiles = @{}
15
16 # Maximum number of jobs to run in parallel
17 $MaxParallelJobs = 6
18
19 # Wait time between check for system load
20 $SecondsBetweenLoadChecks = 10
21
22 $ValidatePackage = {
23   param( 
24     [string] $PackagePath                                 # Full path to a Symbols.NuGet package
25   )
26
27   . $using:PSScriptRoot\..\tools.ps1
28
29   # Ensure input file exist
30   if (!(Test-Path $PackagePath)) {
31     Write-Host "Input file does not exist: $PackagePath"
32     return 1
33   }
34
35   # Extensions for which we'll look for SourceLink information
36   # For now we'll only care about Portable & Embedded PDBs
37   $RelevantExtensions = @(".dll", ".exe", ".pdb")
38  
39   Write-Host -NoNewLine "Validating" ([System.IO.Path]::GetFileName($PackagePath)) "... "
40
41   $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath)
42   $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId
43   $FailedFiles = 0
44
45   Add-Type -AssemblyName System.IO.Compression.FileSystem
46
47   [System.IO.Directory]::CreateDirectory($ExtractPath)  | Out-Null
48
49   try {
50     $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath)
51
52     $zip.Entries | 
53       Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} |
54         ForEach-Object {
55           $FileName = $_.FullName
56           $Extension = [System.IO.Path]::GetExtension($_.Name)
57           $FakeName = -Join((New-Guid), $Extension)
58           $TargetFile = Join-Path -Path $ExtractPath -ChildPath $FakeName 
59
60           # We ignore resource DLLs
61           if ($FileName.EndsWith(".resources.dll")) {
62             return
63           }
64
65           [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true)
66
67           $ValidateFile = {
68             param( 
69               [string] $FullPath,                                # Full path to the module that has to be checked
70               [string] $RealPath,
71               [ref] $FailedFiles
72             )
73
74             $sourcelinkExe = "$env:USERPROFILE\.dotnet\tools"
75             $sourcelinkExe = Resolve-Path "$sourcelinkExe\sourcelink.exe"
76             $SourceLinkInfos = & $sourcelinkExe print-urls $FullPath | Out-String
77
78             if ($LASTEXITCODE -eq 0 -and -not ([string]::IsNullOrEmpty($SourceLinkInfos))) {
79               $NumFailedLinks = 0
80
81               # We only care about Http addresses
82               $Matches = (Select-String '(http[s]?)(:\/\/)([^\s,]+)' -Input $SourceLinkInfos -AllMatches).Matches
83
84               if ($Matches.Count -ne 0) {
85                 $Matches.Value |
86                   ForEach-Object {
87                     $Link = $_
88                     $CommitUrl = "https://raw.githubusercontent.com/${using:GHRepoName}/${using:GHCommit}/"
89                     
90                     $FilePath = $Link.Replace($CommitUrl, "")
91                     $Status = 200
92                     $Cache = $using:RepoFiles
93
94                     if ( !($Cache.ContainsKey($FilePath)) ) {
95                       try {
96                         $Uri = $Link -as [System.URI]
97                       
98                         # Only GitHub links are valid
99                         if ($Uri.AbsoluteURI -ne $null -and ($Uri.Host -match "github" -or $Uri.Host -match "githubusercontent")) {
100                           $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode
101                         }
102                         else {
103                           $Status = 0
104                         }
105                       }
106                       catch {
107                         write-host $_
108                         $Status = 0
109                       }
110                     }
111
112                     if ($Status -ne 200) {
113                       if ($NumFailedLinks -eq 0) {
114                         if ($FailedFiles.Value -eq 0) {
115                           Write-Host
116                         }
117
118                         Write-Host "`tFile $RealPath has broken links:"
119                       }
120
121                       Write-Host "`t`tFailed to retrieve $Link"
122
123                       $NumFailedLinks++
124                     }
125                   }
126               }
127
128               if ($NumFailedLinks -ne 0) {
129                 $FailedFiles.value++
130                 $global:LASTEXITCODE = 1
131               }
132             }
133           }
134         
135           &$ValidateFile $TargetFile $FileName ([ref]$FailedFiles)
136         }
137   }
138   catch {
139   
140   }
141   finally {
142     $zip.Dispose() 
143   }
144
145   if ($FailedFiles -eq 0) {
146     Write-Host "Passed."
147     return 0
148   }
149   else {
150     Write-Host "$PackagePath has broken SourceLink links."
151     return 1
152   }
153 }
154
155 function ValidateSourceLinkLinks {
156   if ($GHRepoName -ne "" -and !($GHRepoName -Match "^[^\s\/]+/[^\s\/]+$")) {
157     if (!($GHRepoName -Match "^[^\s-]+-[^\s]+$")) {
158       Write-PipelineTaskError "GHRepoName should be in the format <org>/<repo> or <org>-<repo>. '$GHRepoName'"
159       ExitWithExitCode 1
160     }
161     else {
162       $GHRepoName = $GHRepoName -replace '^([^\s-]+)-([^\s]+)$', '$1/$2';
163     }
164   }
165
166   if ($GHCommit -ne "" -and !($GHCommit -Match "^[0-9a-fA-F]{40}$")) {
167     Write-PipelineTaskError "GHCommit should be a 40 chars hexadecimal string. '$GHCommit'"
168     ExitWithExitCode 1
169   }
170
171   if ($GHRepoName -ne "" -and $GHCommit -ne "") {
172     $RepoTreeURL = -Join("http://api.github.com/repos/", $GHRepoName, "/git/trees/", $GHCommit, "?recursive=1")
173     $CodeExtensions = @(".cs", ".vb", ".fs", ".fsi", ".fsx", ".fsscript")
174
175     try {
176       # Retrieve the list of files in the repo at that particular commit point and store them in the RepoFiles hash
177       $Data = Invoke-WebRequest $RepoTreeURL -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty tree
178   
179       foreach ($file in $Data) {
180         $Extension = [System.IO.Path]::GetExtension($file.path)
181
182         if ($CodeExtensions.Contains($Extension)) {
183           $RepoFiles[$file.path] = 1
184         }
185       }
186     }
187     catch {
188       Write-Host "Problems downloading the list of files from the repo. Url used: $RepoTreeURL . Execution will proceed without caching."
189     }
190   }
191   elseif ($GHRepoName -ne "" -or $GHCommit -ne "") {
192     Write-Host "For using the http caching mechanism both GHRepoName and GHCommit should be informed."
193   }
194   
195   if (Test-Path $ExtractPath) {
196     Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue
197   }
198
199   # Process each NuGet package in parallel
200   Get-ChildItem "$InputPath\*.symbols.nupkg" |
201     ForEach-Object {
202       Start-Job -ScriptBlock $ValidatePackage -ArgumentList $_.FullName | Out-Null
203       $NumJobs = @(Get-Job -State 'Running').Count
204       
205       while ($NumJobs -ge $MaxParallelJobs) {
206         Write-Host "There are $NumJobs validation jobs running right now. Waiting $SecondsBetweenLoadChecks seconds to check again."
207         sleep $SecondsBetweenLoadChecks
208         $NumJobs = @(Get-Job -State 'Running').Count
209       }
210
211       foreach ($Job in @(Get-Job -State 'Completed')) {
212         Receive-Job -Id $Job.Id
213         Remove-Job -Id $Job.Id
214       }
215     }
216
217   $ValidationFailures = 0
218   foreach ($Job in $Jobs) {
219     $jobResult = Wait-Job -Id $Job.Id | Receive-Job
220     if ($jobResult -ne "0") {
221       $ValidationFailures++
222     }
223   }
224   if ($ValidationFailures -gt 0) {
225     Write-PipelineTaskError " $ValidationFailures package(s) failed validation."
226     ExitWithExitCode 1
227   }
228 }
229
230 function InstallSourcelinkCli {
231   $sourcelinkCliPackageName = "sourcelink"
232
233   $dotnetRoot = InitializeDotNetCli -install:$true
234   $dotnet = "$dotnetRoot\dotnet.exe"
235   $toolList = & "$dotnet" tool list --global
236
237   if (($toolList -like "*$sourcelinkCliPackageName*") -and ($toolList -like "*$sourcelinkCliVersion*")) {
238     Write-Host "SourceLink CLI version $sourcelinkCliVersion is already installed."
239   }
240   else {
241     Write-Host "Installing SourceLink CLI version $sourcelinkCliVersion..."
242     Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed."
243     & "$dotnet" tool install $sourcelinkCliPackageName --version $sourcelinkCliVersion --verbosity "minimal" --global 
244   }
245 }
246
247 try {
248   InstallSourcelinkCli
249
250   ValidateSourceLinkLinks 
251 }
252 catch {
253   Write-Host $_
254   Write-Host $_.Exception
255   Write-Host $_.ScriptStackTrace
256   ExitWithExitCode 1
257 }