2
0

CodeCheck.ps1 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. #requires -version 5
  2. <#
  3. .SYNOPSIS
  4. This script runs a quick check for common errors, such as checking that Visual Studio solutions are up to date or that generated code has been committed to source.
  5. #>
  6. param(
  7. [switch]$ci,
  8. # Optional arguments that enable downloading an internal
  9. # runtime or runtime from a non-default location
  10. [Alias('DotNetRuntimeSourceFeed')]
  11. [string]$RuntimeSourceFeed,
  12. [Alias('DotNetRuntimeSourceFeedKey')]
  13. [string]$RuntimeSourceFeedKey
  14. )
  15. $ErrorActionPreference = 'Stop'
  16. Set-StrictMode -Version 1
  17. Import-Module -Scope Local -Force "$PSScriptRoot/common.psm1"
  18. $repoRoot = Resolve-Path "$PSScriptRoot/../.."
  19. [string[]] $errors = @()
  20. function LogError {
  21. param(
  22. [Parameter(Mandatory = $true, Position = 0)]
  23. [string]$message,
  24. [string]$FilePath,
  25. [string]$LineNumber, # Ignored if -FilePath not specified.
  26. [string]$Code
  27. )
  28. if ($env:TF_BUILD) {
  29. $prefix = "##vso[task.logissue type=error"
  30. if ($FilePath) {
  31. $prefix = "${prefix};sourcepath=$FilePath"
  32. if ($LineNumber) {
  33. $prefix = "${prefix};linenumber=$LineNumber"
  34. }
  35. }
  36. if ($Code) {
  37. $prefix = "${prefix};code=$Code"
  38. }
  39. Write-Host "${prefix}]${message}"
  40. }
  41. $fullMessage = "error ${Code}: $message"
  42. if ($FilePath) {
  43. $fullMessage += " [$FilePath"
  44. if ($LineNumber) {
  45. $fullMessage += ":$LineNumber"
  46. }
  47. $fullMessage += "]"
  48. }
  49. Write-Host -f Red $fullMessage
  50. $script:errors += $fullMessage
  51. }
  52. try {
  53. if ($ci) {
  54. # Install dotnet.exe
  55. if ($RuntimeSourceFeed -or $RuntimeSourceFeedKey) {
  56. & $repoRoot/restore.cmd -ci -nobl -noBuildNodeJS -RuntimeSourceFeed $RuntimeSourceFeed `
  57. -RuntimeSourceFeedKey $RuntimeSourceFeedKey
  58. } else {
  59. & $repoRoot/restore.cmd -ci -nobl -noBuildNodeJS
  60. }
  61. }
  62. . "$repoRoot/activate.ps1"
  63. #
  64. # Duplicate .csproj files can cause issues with a shared build output folder
  65. #
  66. $projectFileNames = New-Object 'System.Collections.Generic.HashSet[string]'
  67. # Ignore duplicates in submodules. These should be isolated from the rest of the build.
  68. # Ignore duplicates in the .ref folder. This is expected.
  69. Get-ChildItem -Recurse "$repoRoot/src/*.*proj" |
  70. Where-Object {
  71. $_.FullName -NotLike '*\submodules\*' -and $_.FullName -NotLike '*\node_modules\*' -and
  72. $_.FullName -NotLike '*\bin\*' -and $_.FullName -NotLike '*\src\ProjectTemplates\*\content\*'
  73. } |
  74. Where-Object { (Split-Path -Leaf (Split-Path -Parent $_)) -ne 'ref' } |
  75. ForEach-Object {
  76. $fileName = [io.path]::GetFileNameWithoutExtension($_)
  77. if (-not ($projectFileNames.Add($fileName))) {
  78. LogError -code 'BUILD003' -filepath $_ `
  79. ("Multiple project files named '$fileName' exist. Project files should have a unique name " +
  80. "to avoid conflicts in build output.")
  81. }
  82. }
  83. #
  84. # Check for unexpected (not from dotnet-public-npm) npm resolutions in lock files.
  85. #
  86. $registry = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/'
  87. Get-ChildItem src\package-lock.json -Recurse |
  88. ForEach-Object FullName |
  89. Where-Object {$_ -NotLike '*\node_modules\*'} |
  90. ForEach-Object {
  91. # -List to limit complaints to one per file.
  92. Select-String '^ resolved ' $_ | Select-String -List -NotMatch $registry
  93. } |
  94. ForEach-Object {
  95. LogError -filePath "${_.Path}" -lineNumber $_.LineNumber `
  96. "Packages in package-lock.json file resolved from wrong registry. All dependencies must be resolved from $registry"
  97. }
  98. #
  99. # Versions.props and Version.Details.xml
  100. #
  101. Write-Host "Checking that Versions.props and Version.Details.xml match"
  102. [xml] $versionProps = Get-Content "$repoRoot/eng/Versions.props"
  103. [xml] $versionDetails = Get-Content "$repoRoot/eng/Version.Details.xml"
  104. $globalJson = Get-Content $repoRoot/global.json | ConvertFrom-Json
  105. $versionVars = New-Object 'System.Collections.Generic.HashSet[string]'
  106. foreach ($vars in $versionProps.SelectNodes("//PropertyGroup[`@Label=`"Automated`"]/*")) {
  107. $versionVars.Add($vars.Name) | Out-Null
  108. }
  109. foreach ($dep in $versionDetails.SelectNodes('//Dependency')) {
  110. Write-Verbose "Found $dep"
  111. $expectedVersion = $dep.Version
  112. if ($dep.Name -in $globalJson.'msbuild-sdks'.PSObject.Properties.Name) {
  113. $actualVersion = $globalJson.'msbuild-sdks'.($dep.Name)
  114. if ($expectedVersion -ne $actualVersion) {
  115. LogError -filepath "$repoRoot\global.json" `
  116. ("MSBuild SDK version '$($dep.Name)' in global.json does not match the value in " +
  117. "Version.Details.xml. Expected '$expectedVersion', actual '$actualVersion'")
  118. }
  119. }
  120. else {
  121. $varName = $dep.Name -replace '\.',''
  122. $varName = $varName -replace '\-',''
  123. $varName = "${varName}Version"
  124. $versionVar = $versionProps.SelectSingleNode("//PropertyGroup[`@Label=`"Automated`"]/$varName")
  125. $actualVersion = $versionVar.InnerText
  126. $versionVars.Remove($varName) | Out-Null
  127. if (-not $versionVar) {
  128. LogError "Missing version variable '$varName' in the 'Automated' property group in $repoRoot/eng/Versions.props"
  129. continue
  130. }
  131. if ($expectedVersion -ne $actualVersion) {
  132. LogError -filepath "$repoRoot\eng\Versions.props" `
  133. ("Version variable '$varName' does not match the value in Version.Details.xml. " +
  134. "Expected '$expectedVersion', actual '$actualVersion'")
  135. }
  136. }
  137. }
  138. foreach ($unexpectedVar in $versionVars) {
  139. LogError -Filepath "$repoRoot\eng\Versions.props" `
  140. ("Version variable '$unexpectedVar' does not have a matching entry in Version.Details.xml. " +
  141. "See https://github.com/dotnet/aspnetcore/blob/main/docs/ReferenceResolution.md for instructions " +
  142. "on how to add a new dependency.")
  143. }
  144. # ComponentsWebAssembly-CSharp.sln is used by the templating engine; MessagePack.sln is irrelevant (in submodule).
  145. $solution = Get-ChildItem "$repoRoot/AspNetCore.sln"
  146. $solutionFile = Split-Path -Leaf $solution
  147. Write-Host "Checking that $solutionFile is up to date"
  148. # $solutionProjects will store relative paths i.e. the exact solution and solution filter content.
  149. $solutionProjects = New-Object 'System.Collections.Generic.HashSet[string]'
  150. # Where-Object needed to ignore heading `dotnet sln` outputs
  151. & dotnet sln $solution list | Where-Object { $_ -like '*proj' } | ForEach-Object {
  152. $proj = Join-Path $repoRoot $_
  153. if (-not ($solutionProjects.Add($_))) {
  154. LogError "Duplicate project. $solutionFile references a project more than once: $proj."
  155. }
  156. if (-not (Test-Path $proj)) {
  157. LogError "Missing project. $solutionFile references a project which does not exist: $proj."
  158. }
  159. }
  160. Write-Host "Checking solution filters"
  161. Get-ChildItem -Recurse "$repoRoot\*.slnf" | ForEach-Object {
  162. $solutionFilter = $_
  163. $json = Get-Content -Raw -Path $solutionFilter |ConvertFrom-Json
  164. $json.solution.projects | ForEach-Object {
  165. if (!$solutionProjects.Contains($_)) {
  166. LogError "$solutionFilter references a project not in $solutionFile`: $_"
  167. }
  168. }
  169. }
  170. #
  171. # Generated code check
  172. #
  173. Write-Host "Re-running code generation"
  174. Write-Host " Re-generating project lists"
  175. Invoke-Block {
  176. & $PSScriptRoot\GenerateProjectList.ps1 -ci:$ci
  177. }
  178. Write-Host " Re-generating package baselines"
  179. Invoke-Block {
  180. & dotnet run --project "$repoRoot/eng/tools/BaselineGenerator/"
  181. }
  182. Write-Host "Running git diff to check for pending changes"
  183. # Redirect stderr to stdout because PowerShell does not consistently handle output to stderr
  184. $changedFiles = & cmd /c 'git --no-pager diff --ignore-space-change --name-only 2>nul'
  185. # Temporary: Disable check for blazor js file and nuget.config (updated automatically for
  186. # internal builds)
  187. $changedFilesExclusions = @("src/Components/Web.JS/dist/Release/blazor.server.js", "src/Components/Web.JS/dist/Release/blazor.web.js", "NuGet.config")
  188. if ($changedFiles) {
  189. foreach ($file in $changedFiles) {
  190. if ($changedFilesExclusions -contains $file) {continue}
  191. $filePath = Resolve-Path "${repoRoot}/${file}"
  192. LogError -filepath $filePath `
  193. ("Generated code is not up to date in $file. You might need to regenerate the reference " +
  194. "assemblies or project list (see docs/ReferenceResolution.md)")
  195. & git --no-pager diff --ignore-space-change $filePath
  196. }
  197. }
  198. $targetBranch = $env:SYSTEM_PULLREQUEST_TARGETBRANCH
  199. if (![string]::IsNullOrEmpty($targetBranch)) {
  200. if ($targetBranch.StartsWith('refs/heads/')) {
  201. $targetBranch = $targetBranch.Replace('refs/heads/','')
  202. }
  203. # Retrieve the set of changed files compared to main
  204. Write-Host "Checking for changes to API baseline files $targetBranch"
  205. $changedFilesFromTarget = git --no-pager diff origin/$targetBranch --ignore-space-change --name-only --diff-filter=ar
  206. $changedAPIBaselines = [System.Collections.Generic.List[string]]::new()
  207. if ($changedFilesFromTarget) {
  208. foreach ($file in $changedFilesFromTarget) {
  209. # Check for changes in Shipped in all branches
  210. if ($file -like '*PublicAPI.Shipped.txt') {
  211. if (!$file.Contains('DevServer/src/PublicAPI.Shipped.txt')) {
  212. $changedAPIBaselines.Add($file)
  213. }
  214. }
  215. # Check for changes in Unshipped in servicing branches
  216. if ($targetBranch -like 'release*' -and $targetBranch -notlike '*preview*' -and $targetBranch -notlike '*rc1*' -and $targetBranch -notlike '*rc2*' -and $file -like '*PublicAPI.Unshipped.txt') {
  217. $changedAPIBaselines.Add($file)
  218. }
  219. }
  220. }
  221. Write-Host "Found changes in $($changedAPIBaselines.count) API baseline files"
  222. if ($changedAPIBaselines.count -gt 0) {
  223. LogError ("Detected modification to baseline API files. PublicAPI.Shipped.txt files should only " +
  224. "be updated after a major release, and PublicAPI.Unshipped.txt files should not " +
  225. "be updated in release branches. See /docs/APIBaselines.md for more information.")
  226. LogError "Modified API baseline files:"
  227. foreach ($file in $changedAPIBaselines) {
  228. LogError $file
  229. }
  230. }
  231. }
  232. }
  233. finally {
  234. Write-Host ""
  235. Write-Host "Summary:"
  236. Write-Host ""
  237. Write-Host " $($errors.Length) error(s)"
  238. Write-Host ""
  239. foreach ($err in $errors) {
  240. Write-Host -f Red $err
  241. }
  242. if ($errors) {
  243. exit 1
  244. }
  245. }