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