<# .SYNOPSIS Runs the clang-tidy check that gates pull requests (.github/workflows/clang_tidy.yml) on your branch. Windows. .DESCRIPTION Configures a separate build directory (build-tidy) without the precompiled header, installs the pinned clang-tidy into a virtual environment inside it, and runs scripts/clang_tidy_diff.py the way CI does. Uncommitted changes are checked too. CI runs on Linux. Here clang-tidy also sees Windows-only code and MSVC's standard library, so it can report findings CI does not; a change that passes here and on Linux passes CI. .EXAMPLE powershell -ExecutionPolicy Bypass -File scripts\run_clang_tidy.ps1 .EXAMPLE powershell -ExecutionPolicy Bypass -File scripts\run_clang_tidy.ps1 -Fix #> param( # Revision to compare against (default: main of the remote that points at # OrcaSlicer/OrcaSlicer, else origin/main). [string]$Base = "", # Do not fetch that remote's main first. [switch]$NoFetch, # Build directory for the compile database. [string]$BuildDir = "build-tidy", # Dependency build directory (default: the deps\build* tree build_win.bat made). [string]$DepsDir = "", # x64 or arm64 (default: this machine's). [string]$Arch = "", # Parallel clang-tidy runs (default: all cores). [int]$Jobs = 0, # Apply clang-tidy's fixes (adds the missing includes). [switch]$Fix, # Install missing tools without asking. [switch]$Yes ) $ErrorActionPreference = "Stop" $Root = Split-Path -Parent $PSScriptRoot Set-Location $Root if (-not [System.IO.Path]::IsPathRooted($BuildDir)) { $BuildDir = Join-Path $Root $BuildDir } # Ask before installing anything. Without a console, or with "no", print the command # instead so it can be run by hand. function Ask([string]$Question) { if ($Yes) { return $true } if ([Console]::IsInputRedirected) { return $false } $reply = Read-Host "$Question [y/N]" return $reply -match '^[Yy]' } function Has([string]$Command) { return [bool](Get-Command $Command -ErrorAction SilentlyContinue) } # Runs a native command with its output, stderr included, streamed to $Log or dropped. function Invoke-Quiet([scriptblock]$Command, [string]$Log) { # Under "Stop", 2>&1 turns every stderr line of a native command into a terminating error. $ErrorActionPreference = "Continue" $lines = { & $Command 2>&1 | ForEach-Object { # "$_" turns a blank stderr line into the text System.Management.Automation.RemoteException. if ($_ -isnot [System.Management.Automation.ErrorRecord]) { $_ } elseif ($null -ne $_.TargetObject) { $_.TargetObject } else { $_.Exception.Message } } } if ($Log) { & $lines | Out-File -Encoding utf8 -LiteralPath $Log } else { & $lines | Out-Null } } function Request-Install([string]$What, [string]$Command) { Write-Host "Missing: $What" if (Ask "Install it now with: $Command ?") { & cmd /c $Command if ($LASTEXITCODE -ne 0) { throw "Installing $What failed." } Write-Host "Installed. Open a new terminal so PATH picks it up, then run this again." } else { Write-Host "To install it yourself, run:" Write-Host " $Command" } exit 1 } if (-not $Arch) { if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { $Arch = "arm64" } else { $Arch = "x64" } } # --- System tools ------------------------------------------------------------- if (-not (Has "git")) { Request-Install "Git" "build_win.bat --install-deps" } # Python: the py launcher, else a python.exe that is not the Microsoft Store stub. $Python = $null if (Has "py") { Invoke-Quiet { & py -3 --version } if ($LASTEXITCODE -eq 0) { $Python = @("py", "-3") } } if (-not $Python -and (Has "python")) { Invoke-Quiet { & python --version } if ($LASTEXITCODE -eq 0) { $Python = @("python") } } if (-not $Python) { Request-Install "Python 3" "winget install -e --id Python.Python.3.12" } $PyExe = $Python[0] $PyArgs = @($Python | Select-Object -Skip 1) # Visual Studio provides the compiler, and its CMake component provides CMake and Ninja. $Vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" $VsPath = $null if (Test-Path $Vswhere) { $VsPath = & $Vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath } if (-not $VsPath) { Request-Install "Visual Studio with the C++ tools" "build_win.bat --install-vs buildtools" } # Load the developer environment, as build_win.bat does. $HostArch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" } $VsDevCmd = Join-Path $VsPath "Common7\Tools\VsDevCmd.bat" # Ignores VsDevCmd's exit code, as build_win.bat does, and checks the variables it sets # before applying any, cleared in cmd so values inherited from a developer shell do not count. $envLines = & cmd /c "set VCToolsInstallDir=& set WindowsSdkDir=& `"$VsDevCmd`" -arch=$Arch -host_arch=$HostArch -no_logo >nul 2>nul & set" if (-not ($envLines -match '^VCToolsInstallDir=.') -or -not ($envLines -match '^WindowsSdkDir=.')) { throw "Loading the Visual Studio $Arch environment failed. Run `"$VsDevCmd`" -arch=$Arch -host_arch=$HostArch in cmd to see why." } foreach ($line in $envLines) { $i = $line.IndexOf("=") if ($i -gt 0) { Set-Item -Path ("env:" + $line.Substring(0, $i)) -Value $line.Substring($i + 1) } } if (-not (Has "cmake")) { Request-Install "CMake" "build_win.bat --install-deps" } if (-not (Has "ninja")) { Request-Install "Ninja" "winget install -e --id Ninja-build.Ninja" } # --- clang-tidy --------------------------------------------------------------- $Requirements = Join-Path $Root "scripts\clang_tidy_requirements.txt" $Pinned = ((Get-Content $Requirements | Select-String '^clang-tidy==(.+)$').Matches[0].Groups[1].Value).Trim() $Venv = Join-Path $BuildDir "clang-tidy-venv" $ClangTidy = $env:CLANG_TIDY function Test-Pinned([string]$Exe) { if (-not $Exe -or -not (Test-Path $Exe)) { return $false } return [bool]((& $Exe --version) -match "version $([regex]::Escape($Pinned))") } if (-not $ClangTidy) { $ClangTidy = Join-Path $Venv "Scripts\clang-tidy.exe" if (-not (Test-Pinned $ClangTidy)) { if (Ask "clang-tidy $Pinned (the version CI uses) is not installed. Install it into $Venv?") { New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null & $PyExe @PyArgs -m venv $Venv if ($LASTEXITCODE -ne 0) { throw "Could not create a Python virtual environment." } & "$Venv\Scripts\python.exe" -m pip install --quiet --upgrade pip & "$Venv\Scripts\python.exe" -m pip install --quiet -r $Requirements if ($LASTEXITCODE -ne 0) { throw "Installing clang-tidy $Pinned failed." } } else { Write-Host "To install it yourself, run:" Write-Host " $($Python -join ' ') -m venv $Venv" Write-Host " $Venv\Scripts\python.exe -m pip install -r scripts\clang_tidy_requirements.txt" exit 1 } } } if (-not (Test-Pinned $ClangTidy)) { Write-Warning "$ClangTidy is not clang-tidy $Pinned, so results may differ from CI." } # --- Dependencies ------------------------------------------------------------- # build_win.bat names the tree after the compiler and architecture: deps\build for cl, # deps\build-clang for clang-cl, with -arm64 appended on ARM64. $Suffix = if ($Arch -eq "arm64") { "-arm64" } else { "" } $Compiler = "cl" # A configured build directory remembers where its dependencies are. $Cache = Join-Path $BuildDir "CMakeCache.txt" if (-not $DepsDir -and (Test-Path $Cache)) { $m = Select-String -Path $Cache -Pattern '^DEP_BUILD_DIR:[A-Z]*=(.+)$' | Select-Object -First 1 if ($m) { $DepsDir = $m.Matches[0].Groups[1].Value } } if (-not $DepsDir) { if (Test-Path "deps\build$Suffix\OrcaSlicer_dep") { $DepsDir = "deps\build$Suffix" } elseif (Test-Path "deps\build-clang$Suffix\OrcaSlicer_dep") { $DepsDir = "deps\build-clang$Suffix" } } if ($DepsDir -and ($DepsDir -match 'clang')) { $Compiler = "clang-cl" } if (-not $DepsDir -or -not (Test-Path (Join-Path $DepsDir "OrcaSlicer_dep"))) { $BuildDeps = "build_win.bat -d --arch $Arch" Write-Host "OrcaSlicer's dependencies are not built." if (Ask "Build them now with $BuildDeps? This takes a while.") { & cmd /c $BuildDeps if ($LASTEXITCODE -ne 0) { throw "Building the dependencies failed." } $DepsDir = "deps\build$Suffix" } else { Write-Host "Build them with $BuildDeps, or point to an existing build with -DepsDir." exit 1 } } $DepsDir = (Resolve-Path $DepsDir).Path if ($Compiler -eq "clang-cl" -and -not (Has "clang-cl")) { Request-Install "clang-cl (the Visual Studio C++ Clang tools)" "build_win.bat --install-vs buildtools" } # --- Compile database --------------------------------------------------------- # The same configure as CI, with Ninja so CMake writes compile_commands.json. Forward # slashes keep CMake from reading a backslash as an escape. $cmakeArgs = @("-S", ".", "-B", $BuildDir, "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_C_COMPILER=$Compiler", "-DCMAKE_CXX_COMPILER=$Compiler", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", "-DSLIC3R_PCH=OFF", "-DORCA_TOOLS=ON", "-DBUILD_TESTS=ON", "-DDEP_BUILD_DIR=$($DepsDir -replace '\\', '/')") Write-Host "Configuring $BuildDir with $Compiler" New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null $Log = Join-Path $BuildDir "configure.log" Invoke-Quiet { & cmake @cmakeArgs } $Log if ($LASTEXITCODE -ne 0) { Get-Content -LiteralPath $Log -Tail 20 throw "Configuring failed; the full log is in $Log." } $HashLog = Join-Path $BuildDir "git_commit_hash.log" Invoke-Quiet { & cmake --build $BuildDir --target git_commit_hash_header } $HashLog if ($LASTEXITCODE -ne 0) { Get-Content -LiteralPath $HashLog -Tail 20 throw "Generating git_commit_hash.h failed; the full log is in $HashLog." } # --- Base revision ------------------------------------------------------------ if (-not $Base) { $Remote = "origin" foreach ($line in (& git remote -v)) { if ($line -match '^(\S+)\s+\S*github\.com[:/]OrcaSlicer/OrcaSlicer(\.git)?\s+\(fetch\)') { $Remote = $Matches[1] break } } if (-not $NoFetch) { & git fetch --quiet $Remote main if ($LASTEXITCODE -ne 0) { throw "git fetch $Remote main failed." } } $Base = "$Remote/main" } Write-Host "Comparing against $Base" # --- Run ---------------------------------------------------------------------- $diffArgs = @("scripts\clang_tidy_diff.py", "-p", $BuildDir, "--base", $Base, "--clang-tidy", $ClangTidy) if ($Jobs -gt 0) { $diffArgs += @("-j", "$Jobs") } if ($Fix) { $diffArgs += @("--", "--fix") } & $PyExe @PyArgs @diffArgs exit $LASTEXITCODE