master
in 310 lines 10.3 KB
Raw
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 }