| 1 | <# |
| 2 | # Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | # Licensed under the MIT License. See LICENSE in the project root for license information. |
| 4 | |
| 5 | this file work is a derivative of https://github.com/microsoft/MixedReality-GraphicsTools-Unreal/blob/main/Tools/scripts/Common.psm1 |
| 6 | #> |
| 7 | |
| 8 | <# |
| 9 | .SYNOPSIS |
| 10 | Run clang-format on all source files. |
| 11 | .PARAMETER Path |
| 12 | Path to the directory (or file) to be processed recursively. By default scans the entire repo. |
| 13 | .PARAMETER ClangFormat |
| 14 | Path to clang-format executable, e.g. "C:\Tools\clang-format.exe" |
| 15 | .PARAMETER ModifiedOnly |
| 16 | Scan only files modified in current git checkout. |
| 17 | .PARAMETER Staged |
| 18 | Check only files staged for commit |
| 19 | .PARAMETER ChangesFile |
| 20 | Scan only files listed in provided txt file (one path per line). Paths need to be relative to repo root. |
| 21 | .PARAMETER Verify |
| 22 | Whether to fail if files are not formatted (instead of applying changes). |
| 23 | .PARAMETER NoFail |
| 24 | Do not set RC=1 when errors found, i.e. only report errors in output. |
| 25 | #> |
| 26 | [CmdletBinding()] |
| 27 | param ( |
| 28 | [string]$Path = $null, |
| 29 | [boolean]$DefaultPackagesConfig = $false, |
| 30 | [string]$ClangFormat = $null, |
| 31 | [boolean]$ModifiedOnly = $True, |
| 32 | [boolean]$Staged = $false, |
| 33 | [string]$ChangesFile = $null, |
| 34 | [boolean]$Verify = $false, |
| 35 | [boolean]$NoFail = $false |
| 36 | ) |
| 37 | |
| 38 | # Only check source files |
| 39 | $FilePatterns = "\.(h|cpp|hpp|c|hxx)$" |
| 40 | |
| 41 | $IgnoreFolders = "(out|.git|.vs|.vscode|bin|CMakeFiles|generated|debug|x64|packages|_deps)$" |
| 42 | |
| 43 | # Handle both execution methods: direct PowerShell and powershell.exe script invocation |
| 44 | if ([string]::IsNullOrEmpty($PSScriptRoot)) { |
| 45 | $RepoRoot = (Get-Location).Path |
| 46 | } else { |
| 47 | $RepoRoot = (Resolve-Path "$PSScriptRoot") |
| 48 | } |
| 49 | |
| 50 | <# |
| 51 | .SYNOPSIS |
| 52 | Given the path to the list of raw git changes, returns an array of |
| 53 | those changes rooted in the git root directory. |
| 54 | .DESCRIPTION |
| 55 | For example, the raw git changes will contain lines like: |
| 56 | |
| 57 | Assets/File.cs |
| 58 | |
| 59 | This function will return a list of paths that look like (assuming |
| 60 | that RepoRoot is C:\repo): |
| 61 | |
| 62 | C:\repo\Assets\File.cs |
| 63 | #> |
| 64 | function GetChangedFiles { |
| 65 | [CmdletBinding()] |
| 66 | param( |
| 67 | [string]$Filename, |
| 68 | [string]$RepoRoot |
| 69 | ) |
| 70 | process { |
| 71 | $rawContent = Get-Content -Path $Filename |
| 72 | $processedContent = @() |
| 73 | foreach ($line in $rawContent) { |
| 74 | $joinedPath = Join-Path -Path $RepoRoot -ChildPath $line |
| 75 | $processedContent += $joinedPath |
| 76 | } |
| 77 | $processedContent |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | if ([string]::IsNullOrEmpty($Path)) { |
| 82 | $Path = $RepoRoot |
| 83 | } |
| 84 | |
| 85 | $ModifiedFiles = $null |
| 86 | |
| 87 | if ($ChangesFile) { |
| 88 | if (-not (Test-Path -Path $ChangesFile -PathType Leaf)) { |
| 89 | Write-Host -ForegroundColor Red "ChangesFile not found: $ChangesFile" |
| 90 | Write-Host "Checking all source files" |
| 91 | } |
| 92 | else { |
| 93 | $ModifiedFiles = GetChangedFiles -Filename $ChangesFile -RepoRoot $RepoRoot |
| 94 | if (($null -eq $ModifiedFiles) -or ($ModifiedFiles.Count -eq 0)) { |
| 95 | Write-Host -ForegroundColor Green "No modified files to format." |
| 96 | exit 0 |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | elseif ($ModifiedOnly -or $Staged) { |
| 101 | $ModifiedFiles = @() |
| 102 | Push-Location -Path $RepoRoot |
| 103 | $Success = $False |
| 104 | try { |
| 105 | if ($Staged) { |
| 106 | $Status = (& git diff-index --cached --name-only HEAD) |
| 107 | $Success = ($LASTEXITCODE -eq 0) |
| 108 | $Status | ForEach-Object { |
| 109 | $FullPath = Resolve-Path $_ -ErrorAction SilentlyContinue |
| 110 | $FileName = Split-Path -Leaf -Path $FullPath |
| 111 | if ($FileName -match $FilePatterns) { |
| 112 | $ModifiedFiles += $FullPath |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | else { |
| 117 | $Status = (& git status --porcelain) |
| 118 | $Success = ($LASTEXITCODE -eq 0) |
| 119 | $Status | ForEach-Object { |
| 120 | $FullPath = (Resolve-Path ($_.Trim() -split " ", 2)[-1] -ErrorAction SilentlyContinue) |
| 121 | $FileName = Split-Path -Leaf -Path $FullPath |
| 122 | if ($FileName -match $FilePatterns) { |
| 123 | $ModifiedFiles += $FullPath |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | catch { |
| 129 | # empty |
| 130 | } |
| 131 | if (-not $Success) { |
| 132 | Write-Host -ForegroundColor Red "Could not get the list of modified files. Check if git is configured correctly." |
| 133 | exit 1 |
| 134 | } |
| 135 | Pop-Location |
| 136 | if (($null -eq $ModifiedFiles) -or ($ModifiedFiles.Count -eq 0)) { |
| 137 | Write-Host -ForegroundColor Green "No modified files to format." |
| 138 | exit 0 |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | $ClangFormat = "${LLVM_INSTALL_DIR}/clang-format.exe" |
| 143 | if (-not (Test-Path -Type Leaf -Path $ClangFormat)) { |
| 144 | # The artifact directory is only known by cmake. Let cmake drop a link/copy where we can find it. |
| 145 | $ClangFormat = "$PSScriptRoot\clang-format.exe" |
| 146 | } |
| 147 | |
| 148 | if (-not (Test-Path -Type Leaf -Path $ClangFormat)) { |
| 149 | Write-Host -ForegroundColor Red "clang-format.exe not found, use cmake to grab llvm package or specify directly with -ClangFormat" |
| 150 | exit 1 |
| 151 | } |
| 152 | |
| 153 | function Collect-FilesToFormat { |
| 154 | [CmdletBinding()] |
| 155 | param ( |
| 156 | [Parameter(Mandatory = $True)] |
| 157 | [string]$Path, |
| 158 | [string[]]$ModifiedFiles = $null |
| 159 | ) |
| 160 | process { |
| 161 | $Path = Resolve-Path $Path |
| 162 | $files = [System.Collections.Generic.List[string]]::new() |
| 163 | if ((Get-Item -Path $Path) -is [System.IO.DirectoryInfo]) { |
| 164 | Get-ChildItem -Path $Path -File ` |
| 165 | | Where-Object { $_.Name -match $FilePatterns } ` |
| 166 | | ForEach-Object { |
| 167 | $FilePath = "$Path\$($_.Name)" |
| 168 | if (($null -eq $ModifiedFiles) -or ($ModifiedFiles -contains $FilePath)) { |
| 169 | if (!($FilePath -match "Intermediate")) { |
| 170 | [void]$files.Add($FilePath) |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | Get-ChildItem -Path $Path -Directory ` |
| 175 | | Where-Object { $_.Name -notmatch $IgnoreFolders } ` |
| 176 | | ForEach-Object { |
| 177 | $subFiles = @(Collect-FilesToFormat -Path "$Path\$($_.Name)" ` |
| 178 | -ModifiedFiles $ModifiedFiles) |
| 179 | foreach ($f in $subFiles) { |
| 180 | [void]$files.Add($f) |
| 181 | } |
| 182 | } |
| 183 | } |
| 184 | else { |
| 185 | [void]$files.Add("$Path") |
| 186 | } |
| 187 | return @($files) |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | $allFiles = @(Collect-FilesToFormat -Path $Path -ModifiedFiles $ModifiedFiles) |
| 192 | |
| 193 | if ($allFiles.Count -eq 0) { |
| 194 | Write-Host -ForegroundColor Green "No files to format." |
| 195 | exit 0 |
| 196 | } |
| 197 | |
| 198 | $threadCount = [Environment]::ProcessorCount |
| 199 | $totalFiles = $allFiles.Count |
| 200 | Write-Host "[clang-format] Checking $totalFiles files using $threadCount threads..." |
| 201 | |
| 202 | # Create runspace pool for parallel execution (one thread per CPU core) |
| 203 | $pool = [RunspaceFactory]::CreateRunspacePool(1, $threadCount) |
| 204 | $pool.Open() |
| 205 | |
| 206 | $formatScript = { |
| 207 | param($clangFormatExe, $filePath, $verifyMode) |
| 208 | $pinfo = [System.Diagnostics.ProcessStartInfo]::new() |
| 209 | $pinfo.FileName = $clangFormatExe |
| 210 | if ($verifyMode) { |
| 211 | $pinfo.Arguments = "--style=file -Werror --dry-run `"$filePath`"" |
| 212 | } |
| 213 | else { |
| 214 | $pinfo.Arguments = "--style=file -Werror -i `"$filePath`"" |
| 215 | } |
| 216 | $pinfo.RedirectStandardError = $true # Not redirecting stdout to avoid a pipe deadlock. |
| 217 | $pinfo.UseShellExecute = $false |
| 218 | |
| 219 | $proc = [System.Diagnostics.Process]::new() |
| 220 | $proc.StartInfo = $pinfo |
| 221 | [void]$proc.Start() |
| 222 | $stderr = $proc.StandardError.ReadToEnd() |
| 223 | $proc.WaitForExit() |
| 224 | |
| 225 | [PSCustomObject]@{ |
| 226 | File = $filePath |
| 227 | ExitCode = $proc.ExitCode |
| 228 | Output = $stderr.TrimEnd() |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | # Submit all jobs to the runspace pool |
| 233 | $jobs = [System.Collections.ArrayList]::new() |
| 234 | foreach ($file in $allFiles) { |
| 235 | $ps = [PowerShell]::Create() |
| 236 | $ps.RunspacePool = $pool |
| 237 | [void]$ps.AddScript($formatScript).AddArgument($ClangFormat).AddArgument($file).AddArgument($Verify) |
| 238 | $handle = $ps.BeginInvoke() |
| 239 | [void]$jobs.Add([PSCustomObject]@{ |
| 240 | PowerShell = $ps |
| 241 | Handle = $handle |
| 242 | }) |
| 243 | } |
| 244 | |
| 245 | # Determine console width for progress display |
| 246 | try { $lineWidth = [Math]::Max(([Console]::BufferWidth - 1), 40) } catch { $lineWidth = 120 } |
| 247 | |
| 248 | $Success = $true |
| 249 | |
| 250 | try { |
| 251 | while ($jobs.Count -gt 0) { |
| 252 | $finishedJobs = @($jobs | Where-Object { $_.Handle.IsCompleted }) |
| 253 | foreach ($job in $finishedJobs) { |
| 254 | try { |
| 255 | $result = $job.PowerShell.EndInvoke($job.Handle)[0] |
| 256 | } |
| 257 | catch { |
| 258 | $result = [PSCustomObject]@{ File = "unknown"; ExitCode = 1; Output = $_.Exception.Message } |
| 259 | } |
| 260 | $job.PowerShell.Dispose() |
| 261 | [void]$jobs.Remove($job) |
| 262 | |
| 263 | if ($result.ExitCode -ne 0) { |
| 264 | $Success = $false |
| 265 | # Clear progress line and print error permanently |
| 266 | [Console]::Write("`r" + (" " * $lineWidth) + "`r") |
| 267 | Write-Host -ForegroundColor Red "[clang-format] $($result.File)" |
| 268 | if ($result.Output) { |
| 269 | Write-Host $result.Output |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | # Update progress line in-place with latest file |
| 274 | $completed = $totalFiles - $jobs.Count |
| 275 | $status = "[clang-format] ($completed/$totalFiles) $($result.File)" |
| 276 | if ($status.Length -gt $lineWidth) { |
| 277 | $status = $status.Substring(0, $lineWidth) |
| 278 | } |
| 279 | [Console]::Write("`r" + $status.PadRight($lineWidth)) |
| 280 | } |
| 281 | Start-Sleep -Milliseconds 50 |
| 282 | } |
| 283 | } |
| 284 | finally { |
| 285 | # Clean up any remaining jobs and the pool |
| 286 | foreach ($job in $jobs) { |
| 287 | $job.PowerShell.Stop() |
| 288 | $job.PowerShell.Dispose() |
| 289 | } |
| 290 | |
| 291 | # Clear final progress line |
| 292 | [Console]::Write("`r" + (" " * $lineWidth) + "`r") |
| 293 | |
| 294 | $pool.Close() |
| 295 | $pool.Dispose() |
| 296 | } |
| 297 | |
| 298 | if ($Success) { |
| 299 | Write-Host "Done." |
| 300 | exit 0 |
| 301 | } |
| 302 | else { |
| 303 | Write-Host -ForegroundColor Red "Errors found (see output). Please make sure to resolve all issues before opening a Pull Request." |
| 304 | Write-Host -ForegroundColor Red "Formatting can be applied by running:" |
| 305 | Write-Host -ForegroundColor Red " powershell $PSCommandPath -ModifiedOnly ```$false [-Path <path to file or directory>]" |
| 306 | if ($NoFail) { |
| 307 | exit 0 # do not prevent commit when used in pre-commit hook |
| 308 | } |
| 309 | exit 1 |
| 310 | } |