SourceLinkValidation.ps1 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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] $SourceLinkToolPath, # Full path to directory where dotnet SourceLink CLI was installed
  5. [Parameter(Mandatory=$true)][string] $GHRepoName, # GitHub name of the repo including the Org. E.g., dotnet/arcade
  6. [Parameter(Mandatory=$true)][string] $GHCommit # GitHub commit SHA used to build the packages
  7. )
  8. function ValidateSourceLinkLinks {
  9. if (!($GHRepoName -Match "^[^\s\/]+/[^\s\/]+$")) {
  10. Write-PipelineTelemetryError -Category "Build" -Message "GHRepoName should be in the format <org>/<repo>"
  11. $global:LASTEXITCODE = 1
  12. return
  13. }
  14. if (!($GHCommit -Match "^[0-9a-fA-F]{40}$")) {
  15. Write-PipelineTelemetryError -Category "Build" -Message "GHCommit should be a 40 chars hexadecimal string"
  16. $global:LASTEXITCODE = 1
  17. return
  18. }
  19. $RepoTreeURL = -Join("https://api.github.com/repos/", $GHRepoName, "/git/trees/", $GHCommit, "?recursive=1")
  20. $CodeExtensions = @(".cs", ".vb", ".fs", ".fsi", ".fsx", ".fsscript")
  21. try {
  22. # Retrieve the list of files in the repo at that particular commit point and store them in the RepoFiles hash
  23. $Data = Invoke-WebRequest $RepoTreeURL | ConvertFrom-Json | Select-Object -ExpandProperty tree
  24. foreach ($file in $Data) {
  25. $Extension = [System.IO.Path]::GetExtension($file.path)
  26. if ($CodeExtensions.Contains($Extension)) {
  27. $RepoFiles[$file.path] = 1
  28. }
  29. }
  30. }
  31. catch {
  32. Write-PipelineTelemetryError -Category "Build" -Message "Problems downloading the list of files from the repo. Url used: $RepoTreeURL"
  33. $global:LASTEXITCODE = 1
  34. return
  35. }
  36. if (Test-Path $ExtractPath) {
  37. Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue
  38. }
  39. # Process each NuGet package in parallel
  40. $Jobs = @()
  41. Get-ChildItem "$InputPath\*.symbols.nupkg" |
  42. ForEach-Object {
  43. $Jobs += Start-Job -ScriptBlock $ValidatePackage -ArgumentList $_.FullName
  44. }
  45. foreach ($Job in $Jobs) {
  46. Wait-Job -Id $Job.Id | Receive-Job
  47. }
  48. }
  49. try {
  50. . $PSScriptRoot\pipeline-logging-functions.ps1
  51. # Cache/HashMap (File -> Exist flag) used to consult whether a file exist
  52. # in the repository at a specific commit point. This is populated by inserting
  53. # all files present in the repo at a specific commit point.
  54. $global:RepoFiles = @{}
  55. $ValidatePackage = {
  56. param(
  57. [string] $PackagePath # Full path to a Symbols.NuGet package
  58. )
  59. # Ensure input file exist
  60. if (!(Test-Path $PackagePath)) {
  61. throw "Input file does not exist: $PackagePath"
  62. }
  63. # Extensions for which we'll look for SourceLink information
  64. # For now we'll only care about Portable & Embedded PDBs
  65. $RelevantExtensions = @(".dll", ".exe", ".pdb")
  66. Write-Host -NoNewLine "Validating" ([System.IO.Path]::GetFileName($PackagePath)) "... "
  67. $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath)
  68. $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId
  69. $FailedFiles = 0
  70. Add-Type -AssemblyName System.IO.Compression.FileSystem
  71. [System.IO.Directory]::CreateDirectory($ExtractPath);
  72. $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath)
  73. $zip.Entries |
  74. Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} |
  75. ForEach-Object {
  76. $FileName = $_.FullName
  77. $Extension = [System.IO.Path]::GetExtension($_.Name)
  78. $FakeName = -Join((New-Guid), $Extension)
  79. $TargetFile = Join-Path -Path $ExtractPath -ChildPath $FakeName
  80. # We ignore resource DLLs
  81. if ($FileName.EndsWith(".resources.dll")) {
  82. return
  83. }
  84. [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true)
  85. $ValidateFile = {
  86. param(
  87. [string] $FullPath, # Full path to the module that has to be checked
  88. [string] $RealPath,
  89. [ref] $FailedFiles
  90. )
  91. # Makes easier to reference `sourcelink cli`
  92. Push-Location $using:SourceLinkToolPath
  93. $SourceLinkInfos = .\sourcelink.exe print-urls $FullPath | Out-String
  94. if ($LASTEXITCODE -eq 0 -and -not ([string]::IsNullOrEmpty($SourceLinkInfos))) {
  95. $NumFailedLinks = 0
  96. # We only care about Http addresses
  97. $Matches = (Select-String '(http[s]?)(:\/\/)([^\s,]+)' -Input $SourceLinkInfos -AllMatches).Matches
  98. if ($Matches.Count -ne 0) {
  99. $Matches.Value |
  100. ForEach-Object {
  101. $Link = $_
  102. $CommitUrl = -Join("https://raw.githubusercontent.com/", $using:GHRepoName, "/", $using:GHCommit, "/")
  103. $FilePath = $Link.Replace($CommitUrl, "")
  104. $Status = 200
  105. $Cache = $using:RepoFiles
  106. if ( !($Cache.ContainsKey($FilePath)) ) {
  107. try {
  108. $Uri = $Link -as [System.URI]
  109. # Only GitHub links are valid
  110. if ($Uri.AbsoluteURI -ne $null -and $Uri.Host -match "github") {
  111. $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode
  112. }
  113. else {
  114. $Status = 0
  115. }
  116. }
  117. catch {
  118. $Status = 0
  119. }
  120. }
  121. if ($Status -ne 200) {
  122. if ($NumFailedLinks -eq 0) {
  123. if ($FailedFiles.Value -eq 0) {
  124. Write-Host
  125. }
  126. Write-Host "`tFile $RealPath has broken links:"
  127. }
  128. Write-Host "`t`tFailed to retrieve $Link"
  129. $NumFailedLinks++
  130. }
  131. }
  132. }
  133. if ($NumFailedLinks -ne 0) {
  134. $FailedFiles.value++
  135. $global:LASTEXITCODE = 1
  136. }
  137. }
  138. Pop-Location
  139. }
  140. &$ValidateFile $TargetFile $FileName ([ref]$FailedFiles)
  141. }
  142. $zip.Dispose()
  143. if ($FailedFiles -eq 0) {
  144. Write-Host "Passed."
  145. }
  146. }
  147. Measure-Command { ValidateSourceLinkLinks }
  148. }
  149. catch {
  150. Write-Host $_.ScriptStackTrace
  151. Write-PipelineTelemetryError -Category 'SourceLink' -Message $_
  152. ExitWithExitCode 1
  153. }