<# .SYNOPSIS Compiles or tidies up code from Visual Studio .vcxproj project files. .DESCRIPTION This PowerShell script scans for all .vcxproj Visual Studio projects inside a source directory. One or more of these projects will be compiled or tidied up (modernized), using Clang. .PARAMETER aSolutionsPath Alias 'dir'. Source directory to find sln files. Projects will be extracted from each sln. Important: You can pass an absolute path to a sln. This way, no file searching will be done, and only the projects from this solution file will be taken into account. .PARAMETER aVcxprojToCompile Alias 'proj'. Array of project(s) to compile. If empty, all projects found in solutions are compiled. If the -literal switch is present, name is matched exactly. Otherwise, regex matching is used, e.g. "msicomp" compiles all projects containing 'msicomp'. Absolute disk paths to vcxproj files are accepted. Can be passed as comma separated values. .PARAMETER aVcxprojToIgnore Alias 'proj-ignore'. Array of project(s) to ignore, from the matched ones. If empty, all already matched projects are compiled. If the -literal switch is present, name is matched exactly. Otherwise, regex matching is used, e.g. "msicomp" ignores projects containing 'msicomp'. Can be passed as comma separated values. .PARAMETER aVcxprojConfigPlatform Alias 'active-config'. The configuration-platform pair, separated by |, to be used when processing project files. E.g. 'Debug|Win32'. If not specified, the first configuration-platform found in the current project is used. .PARAMETER aCppToCompile Alias 'file'. What cpp(s) to compile from the found project(s). If empty, all CPPs are compiled. If the -literal switch is present, name is matched exactly. Otherwise, regex matching is used, e.g. "table" compiles all CPPs containing 'table'. Note: If any headers are given then all translation units that include them will be processed. .PARAMETER aCppToIgnore Alias 'file-ignore'. Array of file(s) to ignore, from the matched ones. If empty, all already matched files are compiled. If the -literal switch is present, name is matched exactly. Otherwise, regex matching is used, e.g. "table" ignores all CPPs containing 'table'. Can be passed as comma separated values. .PARAMETER aUseParallelCompile Alias 'parallel'. Switch to run in parallel mode, on all logical CPU cores. .PARAMETER aContinueOnError Alias 'continue'. Switch to continue project compilation even when errors occur. .PARAMETER aTreatAdditionalIncludesAsSystemIncludes Alias 'treat-sai'. Switch to treat project additional include directories as system includes. .PARAMETER aClangCompileFlags Alias 'clang-flags'. Flags given to clang++ when compiling project, alongside project-specific defines. .PARAMETER aDisableNameRegexMatching Alias 'literal'. Switch to take project and cpp name filters literally, not by regex matching. .PARAMETER aTidyFlags Alias 'tidy'. If not empty clang-tidy will be called with given flags, instead of clang++. The tidy operation is applied to whole translation units, meaning all directory headers included in the CPP will be tidied up too. Changes will not be applied, only simulated. If aTidyFixFlags is present, it takes precedence over this parameter. If '.clang-tidy' value is given, configuration will be read from .clang-tidy file in the closest parent directory. .PARAMETER aTidyFixFlags Alias 'tidy-fix'. If not empty clang-tidy will be called with given flags, instead of clang++. The tidy operation is applied to whole translation units, meaning all directory headers included in the CPP will be tidied up too. Changes will be applied to the file(s). If present, this parameter takes precedence over aTidyFlags. If '.clang-tidy' value is given, configuration will be read from .clang-tidy file in the closest parent directory. .PARAMETER aAfterTidyFixFormatStyle Alias 'format-style'. Used in combination with 'tidy-fix'. If present, clang-tidy will also format the fixed file(s), using the specified style. Possible values: - not present, no formatting will be done - 'file' Literally 'file', not a placeholder. Uses .clang-format file in the closest parent directory. - 'llvm' - 'google' - 'webkit' - 'mozilla' .PARAMETER aVisualStudioVersion Alias 'vs-ver'. Version of Visual Studio (VC++) installed and that'll be used for standard library include directories. E.g. 2017. Optional. If not given, it will be inferred based on the project toolset version. .PARAMETER aVisualStudioSku Alias 'vs-sku'. Sku of Visual Studio (VC++) installed and that'll be used for standard library include directories. E.g. Professional. If not given, the first detected Visual Studio SKU will be used. .NOTES Author: Gabriel Diaconita #> #Requires -Version 3 param( [alias("proj")] [Parameter(Mandatory=$false, HelpMessage="Filter project(s) to compile/tidy")] [string[]] $aVcxprojToCompile , [alias("dir")] [Parameter(Mandatory=$false, HelpMessage="Source directory for finding solutions; projects will be found from each sln")] [string] $aSolutionsPath , [alias("proj-ignore")] [Parameter(Mandatory=$false, HelpMessage="Specify projects to ignore")] [string[]] $aVcxprojToIgnore , [alias("active-config")] [Parameter(Mandatory=$false, HelpMessage="Config/platform to be used, e.g. Debug|Win32")] [string] $aVcxprojConfigPlatform , [alias("file")] [Parameter(Mandatory=$false, HelpMessage="Filter file(s) to compile/tidy")] [string[]] $aCppToCompile , [alias("file-ignore")] [Parameter(Mandatory=$false, HelpMessage="Specify file(s) to ignore")] [string[]] $aCppToIgnore , [alias("parallel")] [Parameter(Mandatory=$false, HelpMessage="Compile/tidy projects in parallel")] [switch] $aUseParallelCompile , [alias("continue")] [Parameter(Mandatory=$false, HelpMessage="Allow CPT to continue after encounteringan error")] [switch] $aContinueOnError , [alias("treat-sai")] [Parameter(Mandatory=$false, HelpMessage="Treat project additional include directories as system includes")] [switch] $aTreatAdditionalIncludesAsSystemIncludes , [alias("clang-flags")] [Parameter(Mandatory=$false, HelpMessage="Specify compilation flags to CLANG")] [string[]] $aClangCompileFlags , [alias("literal")] [Parameter(Mandatory=$false, HelpMessage="Disable regex matching for all paths received as script parameters")] [switch] $aDisableNameRegexMatching , [alias("tidy")] [Parameter(Mandatory=$false, HelpMessage="Specify flags to CLANG TIDY")] [string] $aTidyFlags , [alias("tidy-fix")] [Parameter(Mandatory=$false, HelpMessage="Specify flags to CLANG TIDY & FIX")] [string] $aTidyFixFlags , [alias("header-filter")] [Parameter(Mandatory=$false, HelpMessage="Enable Clang-Tidy to run on header files")] [string] $aTidyHeaderFilter , [alias("format-style")] [Parameter(Mandatory=$false, HelpMessage="Used with 'tidy-fix'; tells CLANG TIDY-FIX to also format the fixed file(s)")] [string] $aAfterTidyFixFormatStyle , [alias("vs-ver")] [Parameter(Mandatory=$false, HelpMessage="Version of Visual Studio toolset to use for loading project")] [string] $aVisualStudioVersion = "2017" , [alias("vs-sku")] [Parameter(Mandatory=$false, HelpMessage="Edition of Visual Studio toolset to use for loading project")] [string] $aVisualStudioSku ) # System Architecture Constants # ------------------------------------------------------------------------------------------------ Set-Variable -name kLogicalCoreCount -value ` (@(Get-WmiObject -class Win32_processor) | ` ForEach-Object -Begin { $coreCount = 0 } ` -Process { $coreCount += ($_ | Select-Object -property NumberOfLogicalProcessors ` -ExpandProperty NumberOfLogicalProcessors) } ` -End { $coreCount }) -option Constant # ------------------------------------------------------------------------------------------------ # Return Value Constants Set-Variable -name kScriptFailsExitCode -value 47 -option Constant # ------------------------------------------------------------------------------------------------ # File System Constants Set-Variable -name kExtensionVcxproj -value ".vcxproj" -option Constant Set-Variable -name kExtensionSolution -value ".sln" -option Constant Set-Variable -name kExtensionClangPch -value ".clang.pch" -option Constant Set-Variable -name kExtensionC -value ".c" -option Constant # ------------------------------------------------------------------------------------------------ # Clang-Related Constants Set-Variable -name kClangFlagSupressLINK -value @("-fsyntax-only") -option Constant Set-Variable -name kClangFlagWarningIsError -value @("-Werror") -option Constant Set-Variable -name kClangFlagIncludePch -value "-include-pch" -option Constant Set-Variable -name kClangFlagEmitPch -value "-emit-pch" -option Constant Set-Variable -name kClangFlagMinusO -value "-o" -option Constant Set-Variable -name kClangDefinePrefix -value "-D" -option Constant Set-Variable -name kClangFlagNoUnusedArg -value "-Wno-unused-command-line-argument" ` -option Constant Set-Variable -name kClangFlagNoMsInclude -value "-Wno-microsoft-include" ` -Option Constant Set-Variable -name kClangFlagFileIsCPP -value "-x c++" -option Constant Set-Variable -name kClangFlagFileIsC -value "-x c" -option Constant Set-Variable -name kClangFlagForceInclude -value "-include" -option Constant Set-Variable -name kClangCompiler -value "clang++.exe" -option Constant Set-Variable -name kClangTidy -value "clang-tidy.exe" -option Constant Set-Variable -name kClangTidyFlags -value @("-quiet" ,"--") -option Constant Set-Variable -name kClangTidyFixFlags -value @("-quiet" ,"-fix-errors" , "--") -option Constant Set-Variable -name kClangTidyFlagHeaderFilter -value "-header-filter=" -option Constant Set-Variable -name kClangTidyFlagChecks -value "-checks=" -option Constant Set-Variable -name kClangTidyUseFile -value ".clang-tidy" -option Constant Set-Variable -name kClangTidyFormatStyle -value "-format-style=" -option Constant # ------------------------------------------------------------------------------------------------ # Default install locations of LLVM. If present there, we automatically use it Set-Variable -name kLLVMInstallLocations -value @("${Env:ProgramW6432}\LLVM\bin" ,"${Env:ProgramFiles(x86)}\LLVM\bin" ) -option Constant #------------------------------------------------------------------------------------------------- # Custom Types Add-Type -TypeDefinition @" public enum WorkloadType { Compile, Tidy, TidyFix } "@ @( "$PSScriptRoot\psClang\io.ps1" , "$PSScriptRoot\psClang\visualstudio-detection.ps1" , "$PSScriptRoot\psClang\msbuild-expression-eval.ps1" , "$PSScriptRoot\psClang\msbuild-project-load.ps1" , "$PSScriptRoot\psClang\msbuild-project-data.ps1" , "$PSScriptRoot\psClang\get-header-references.ps1" ) | ForEach-Object { . $_ } #------------------------------------------------------------------------------------------------- # Global variables # temporary files created during project processing (e.g. PCH files) [System.Collections.ArrayList] $global:FilesToDeleteWhenScriptQuits = @() # filePath-fileData for SLN files located in source directory [System.Collections.Generic.Dictionary[String,String]] $global:slnFiles = @{} # flag to signal when errors are encounteres during project processing [Boolean] $global:FoundErrors = $false # default ClangPowerTools version of visual studio to use [string] $global:cptDefaultVisualStudioVersion = "2017" # version of VS currently used [string] $global:cptVisualStudioVersion = $aVisualStudioVersion #------------------------------------------------------------------------------------------------- # Global functions Function Exit-Script([Parameter(Mandatory=$false)][int] $code = 0) { Write-Verbose-Array -array $global:FilesToDeleteWhenScriptQuits ` -name "Cleaning up PCH temporaries" # Clean-up foreach ($file in $global:FilesToDeleteWhenScriptQuits) { Remove-Item $file -ErrorAction SilentlyContinue | Out-Null } # Restore working directory Pop-Location exit $code } Function Fail-Script([parameter(Mandatory=$false)][string] $msg = "Got errors.") { if (![string]::IsNullOrEmpty($msg)) { Write-Err $msg } Exit-Script($kScriptFailsExitCode) } Function Get-SourceDirectory() { [bool] $isDirectory = ($(Get-Item $aSolutionsPath) -is [System.IO.DirectoryInfo]) if ($isDirectory) { return $aSolutionsPath } else { return (Get-FileDirectory -filePath $aSolutionsPath) } } function Load-Solutions() { Write-Verbose "Scanning for solution files" $slns = Get-ChildItem -recurse -LiteralPath "$aSolutionsPath" -Filter "*$kExtensionSolution" foreach ($sln in $slns) { $slnPath = $sln.FullName $global:slnFiles[$slnPath] = (Get-Content $slnPath) } Write-Verbose-Array -array $global:slnFiles.Keys -name "Solution file paths" } function Get-SolutionProjects([Parameter(Mandatory=$true)][string] $slnPath) { [string] $slnDirectory = Get-FileDirectory -file $slnPath $matches = [regex]::Matches($global:slnFiles[$slnPath], 'Project\([{}\"A-Z0-9\-]+\) = \".*?\",\s\"(.*?)\"') $projectAbsolutePaths = $matches ` | ForEach-Object { Canonize-Path -base $slnDirectory ` -child $_.Groups[1].Value.Replace('"','') -ignoreErrors } ` | Where-Object { ! [string]::IsNullOrEmpty($_) -and $_.EndsWith($kExtensionVcxproj) } return $projectAbsolutePaths } function Get-ProjectSolution() { foreach ($slnPath in $global:slnFiles.Keys) { [string[]] $solutionProjectPaths = Get-SolutionProjects $slnPath if ($solutionProjectPaths -and $solutionProjectPaths -contains $global:vcxprojPath) { return $slnPath } } return "" } Function Get-Projects() { [string[]] $projects = @() foreach ($slnPath in $global:slnFiles.Keys) { [string[]] $solutionProjects = Get-SolutionProjects -slnPath $slnPath if ($solutionProjects.Count -gt 0) { $projects += $solutionProjects } } return ($projects | Select -Unique); } Function Get-ClangIncludeDirectories( [Parameter(Mandatory=$false)][string[]] $includeDirectories , [Parameter(Mandatory=$false)][string[]] $additionalIncludeDirectories ) { [string[]] $returnDirs = @() foreach ($includeDir in $includeDirectories) { $returnDirs += "-isystem""$includeDir""" } foreach ($includeDir in $additionalIncludeDirectories) { if ($aTreatAdditionalIncludesAsSystemIncludes) { $returnDirs += "-isystem""$includeDir""" } else { $returnDirs += "-I""$includeDir""" } } return $returnDirs } Function Generate-Pch( [Parameter(Mandatory=$true)] [string] $stdafxDir , [Parameter(Mandatory=$false)][string[]] $includeDirectories , [Parameter(Mandatory=$false)][string[]] $additionalIncludeDirectories , [Parameter(Mandatory=$true)] [string] $stdafxHeaderName , [Parameter(Mandatory=$false)][string[]] $preprocessorDefinitions) { if (Is-CProject) { Write-Verbose "Skipping PCH creation for C project." return "" } [string] $stdafx = (Canonize-Path -base $stdafxDir -child $stdafxHeaderName) [string] $vcxprojShortName = [System.IO.Path]::GetFileNameWithoutExtension($global:vcxprojPath); [string] $stdafxPch = (Join-Path -path (Get-SourceDirectory) ` -ChildPath "$vcxprojShortName$kExtensionClangPch") Remove-Item -Path "$stdafxPch" -ErrorAction SilentlyContinue | Out-Null $global:FilesToDeleteWhenScriptQuits.Add($stdafxPch) | Out-Null # Suppress -Werror for PCH generation as it throws warnings quite often in code we cannot control [string[]] $clangFlags = Get-ClangCompileFlags | Where-Object { $_ -ne $kClangFlagWarningIsError } [string[]] $compilationFlags = @("""$stdafx""" ,$kClangFlagEmitPch ,$kClangFlagMinusO ,"""$stdafxPch""" ,$clangFlags ,$kClangFlagNoUnusedArg ,$preprocessorDefinitions ) $compilationFlags += Get-ClangIncludeDirectories -includeDirectories $includeDirectories ` -additionalIncludeDirectories $additionalIncludeDirectories # Remove empty arguments from the list because Start-Process will complain $compilationFlags = $compilationFlags | Where-Object { $_ } | Select -Unique Write-Verbose "INVOKE: ""$($global:llvmLocation)\$kClangCompiler"" $compilationFlags" [System.Diagnostics.Process] $processInfo = Start-Process -FilePath $kClangCompiler ` -ArgumentList $compilationFlags ` -WorkingDirectory "$(Get-SourceDirectory)" ` -NoNewWindow ` -Wait ` -PassThru if (($processInfo.ExitCode -ne 0) -and (!$aContinueOnError)) { Fail-Script "Errors encountered during PCH creation" } if (Test-Path $stdafxPch) { return $stdafxPch } else { return "" } } Function Get-ExeToCall([Parameter(Mandatory=$true)][WorkloadType] $workloadType) { switch ($workloadType) { "Compile" { return $kClangCompiler } "Tidy" { return $kClangTidy } "TidyFix" { return $kClangTidy } } } Function Get-CompileCallArguments( [Parameter(Mandatory=$false)][string[]] $preprocessorDefinitions , [Parameter(Mandatory=$false)][string[]] $includeDirectories , [Parameter(Mandatory=$false)][string[]] $additionalIncludeDirectories , [Parameter(Mandatory=$false)][string[]] $forceIncludeFiles , [Parameter(Mandatory=$false)][string] $pchFilePath , [Parameter(Mandatory=$true)][string] $fileToCompile) { [string[]] $projectCompileArgs = @() if (! [string]::IsNullOrEmpty($pchFilePath) -and ! $fileToCompile.EndsWith($kExtensionC)) { $projectCompileArgs += @($kClangFlagIncludePch , """$pchFilePath""") } $isCpp = $true $languageFlag = $kClangFlagFileIsCPP if ($fileToCompile.EndsWith($kExtensionC)) { $isCpp = $false $languageFlag = $kClangFlagFileIsC } $projectCompileArgs += @( $languageFlag , """$fileToCompile""" , @(Get-ClangCompileFlags -isCpp $isCpp) , $kClangFlagSupressLINK , $preprocessorDefinitions ) $projectCompileArgs += Get-ClangIncludeDirectories -includeDirectories $includeDirectories ` -additionalIncludeDirectories $additionalIncludeDirectories if ($forceIncludeFiles) { $projectCompileArgs += $kClangFlagNoMsInclude; foreach ($file in $forceIncludeFiles) { $projectCompileArgs += "$kClangFlagForceInclude $file" } } return $projectCompileArgs } Function Get-TidyCallArguments( [Parameter(Mandatory=$false)][string[]] $preprocessorDefinitions , [Parameter(Mandatory=$false)][string[]] $includeDirectories , [Parameter(Mandatory=$false)][string[]] $additionalIncludeDirectories , [Parameter(Mandatory=$false)][string[]] $forceIncludeFiles , [Parameter(Mandatory=$true)][string] $fileToTidy , [Parameter(Mandatory=$false)][string] $pchFilePath , [Parameter(Mandatory=$false)][switch] $fix) { [string[]] $tidyArgs = @("""$fileToTidy""") if ($fix -and $aTidyFixFlags -ne $kClangTidyUseFile) { $tidyArgs += "$kClangTidyFlagChecks$aTidyFixFlags" } elseif ($aTidyFlags -ne $kClangTidyUseFile) { $tidyArgs += "$kClangTidyFlagChecks$aTidyFlags" } # The header-filter flag enables clang-tidy to run on headers too. if (![string]::IsNullOrEmpty($aTidyHeaderFilter)) { if ($aTidyHeaderFilter -eq '_') { [string] $fileNameMatch = """$(Get-FileName -path $fileToTidy -noext).*""" $tidyArgs += "$kClangTidyFlagHeaderFilter$fileNameMatch" } else { $tidyArgs += "$kClangTidyFlagHeaderFilter""$aTidyHeaderFilter""" } } if ($fix) { if (![string]::IsNullOrEmpty($aAfterTidyFixFormatStyle)) { $tidyArgs += "$kClangTidyFormatStyle$aAfterTidyFixFormatStyle" } $tidyArgs += $kClangTidyFixFlags } else { $tidyArgs += $kClangTidyFlags } $tidyArgs += Get-ClangIncludeDirectories -includeDirectories $includeDirectories ` -additionalIncludeDirectories $additionalIncludeDirectories $isCpp = $true $languageFlag = $kClangFlagFileIsCPP if ($fileToTidy.EndsWith($kExtensionC)) { $isCpp = $false $languageFlag = $kClangFlagFileIsC } # We reuse flags used for compilation and preprocessor definitions. $tidyArgs += @(Get-ClangCompileFlags -isCpp $isCpp) $tidyArgs += $preprocessorDefinitions $tidyArgs += $languageFlag if (! [string]::IsNullOrEmpty($pchFilePath) -and ! $fileToTidy.EndsWith($kExtensionC)) { $tidyArgs += @($kClangFlagIncludePch , """$pchFilePath""") } if ($forceIncludeFiles) { $tidyArgs += $kClangFlagNoMsInclude; foreach ($file in $forceIncludeFiles) { $tidyArgs += "$kClangFlagForceInclude $file" } } return $tidyArgs } Function Get-ExeCallArguments( [Parameter(Mandatory=$false)][string] $pchFilePath , [Parameter(Mandatory=$false)][string[]] $includeDirectories , [Parameter(Mandatory=$false)][string[]] $additionalIncludeDirectories , [Parameter(Mandatory=$false)][string[]] $preprocessorDefinitions , [Parameter(Mandatory=$false)][string[]] $forceIncludeFiles , [Parameter(Mandatory=$true) ][string] $currentFile , [Parameter(Mandatory=$true) ][WorkloadType] $workloadType) { switch ($workloadType) { Compile { return Get-CompileCallArguments -preprocessorDefinitions $preprocessorDefinitions ` -includeDirectories $includeDirectories ` -additionalIncludeDirectories $additionalIncludeDirectories ` -forceIncludeFiles $forceIncludeFiles ` -pchFilePath $pchFilePath ` -fileToCompile $currentFile } Tidy { return Get-TidyCallArguments -preprocessorDefinitions $preprocessorDefinitions ` -includeDirectories $includeDirectories ` -additionalIncludeDirectories $additionalIncludeDirectories ` -forceIncludeFiles $forceIncludeFiles ` -pchFilePath $pchFilePath ` -fileToTidy $currentFile } TidyFix { return Get-TidyCallArguments -preprocessorDefinitions $preprocessorDefinitions ` -includeDirectories $includeDirectories ` -additionalIncludeDirectories $additionalIncludeDirectories ` -forceIncludeFiles $forceIncludeFiles ` -pchFilePath $pchFilePath ` -fileToTidy $currentFile ` -fix} } } Function Process-ProjectResult($compileResult) { if (!$compileResult.Success) { Write-Err ($compileResult.Output) if (!$aContinueOnError) { # Wait for other workers to finish. They have a lock on the PCH file Get-Job -state Running | Wait-Job | Remove-Job | Out-Null Fail-Script } $global:FoundErrors = $true } else { if ( $compileResult.Output.Length -gt 0) { Write-Output $compileResult.Output } } } Function Wait-AndProcessBuildJobs([switch]$any) { $runningJobs = @(Get-Job -state Running) if ($any) { $runningJobs | Wait-Job -Any | Out-Null } else { $runningJobs | Wait-Job | Out-Null } $jobData = Get-Job -State Completed foreach ($job in $jobData) { $buildResult = Receive-Job $job Process-ProjectResult -compileResult $buildResult } Remove-Job -State Completed } Function Wait-ForWorkerJobSlot() { # We allow as many background workers as we have logical CPU cores $runningJobs = @(Get-Job -State Running) if ($runningJobs.Count -ge $kLogicalCoreCount) { Wait-AndProcessBuildJobs -any } } Function Run-ClangJobs([Parameter(Mandatory=$true)] $clangJobs) { # Script block (lambda) for logic to be used when running a clang job. $jobWorkToBeDone = ` { param( $job ) Push-Location $job.WorkingDirectory [string] $clangConfigFile = [System.IO.Path]::GetTempFileName() [string] $clangConfigContent = "" if ($job.FilePath -like '*tidy*') { # We have to separate Clang args from Tidy args $splitparams = $job.ArgumentList -split "--" $clangConfigContent = $splitparams[1] $job.ArgumentList = ($splitparams[0] + " -- --config ""$clangConfigFile""") } else { # Tell Clang to take its args from a config file $clangConfigContent = $job.ArgumentList $job.ArgumentList = "--config ""$clangConfigFile""" } # escape slashes for file paths # make sure escaped double quotes are not messed up $clangConfigContent = $clangConfigContent -replace '\\([^"])', '\\$1' # save arguments to clang config file $clangConfigContent > $clangConfigFile # When PowerShell encounters errors, the first one is handled differently from consecutive ones # To circumvent this, do not execute the job directly, but execute it via cmd.exe # See also https://stackoverflow.com/a/35980675 $callOutput = cmd /c $job.FilePath $job.ArgumentList.Split(' ') '2>&1' | Out-String $callSuccess = $LASTEXITCODE -eq 0 Remove-Item $clangConfigFile Pop-Location return New-Object PsObject -Prop @{ "File" = $job.File; "Success" = $callSuccess; "Output" = $callOutput } } [int] $jobCount = $clangJobs.Count [int] $crtJobCount = $jobCount foreach ($job in $clangJobs) { # Check if we must wait for background jobs to complete Wait-ForWorkerJobSlot # Inform console what CPP we are processing next Write-Output "$($crtJobCount): $($job.File)" if ($aUseParallelCompile) { Start-Job -ScriptBlock $jobWorkToBeDone ` -ArgumentList $job ` -ErrorAction Continue | Out-Null } else { $compileResult = Invoke-Command -ScriptBlock $jobWorkToBeDone ` -ArgumentList $job Process-ProjectResult -compileResult $compileResult } $crtJobCount -= 1 } Wait-AndProcessBuildJobs } Function Process-Project( [Parameter(Mandatory=$true)][string] $vcxprojPath , [Parameter(Mandatory=$true)][WorkloadType] $workloadType) { #----------------------------------------------------------------------------------------------- # Load data LoadProject($vcxprojPath) #----------------------------------------------------------------------------------------------- # DETECT PLATFORM TOOLSET [string] $platformToolset = Get-ProjectPlatformToolset Write-Verbose "Platform toolset: $platformToolset" if ( ([int]$platformToolset.Remove(0, 1).Replace("_xp", "")) -le 140) { if ($global:cptVisualStudioVersion -ne '2015') { # we need to reload everything and use VS2015 Write-Verbose "Switching to VS2015 because of v140 toolset. Reloading project..." $global:cptVisualStudioVersion = "2015" LoadProject($vcxprojPath) } } else { if ($global:cptVisualStudioVersion -ne $global:cptDefaultVisualStudioVersion) { # we need to reload everything and the default vs version Write-Verbose "Switching to default VsVer because of toolset. Reloading project..." $global:cptVisualStudioVersion = $global:cptDefaultVisualStudioVersion LoadProject($vcxprojPath) } } #----------------------------------------------------------------------------------------------- # FIND FORCE INCLUDES [string[]] $forceIncludeFiles = Get-ProjectForceIncludes Write-Verbose "Force includes: $forceIncludeFiles" #----------------------------------------------------------------------------------------------- # DETECT PROJECT PREPROCESSOR DEFINITIONS [string[]] $preprocessorDefinitions = Get-ProjectPreprocessorDefines if ($global:cptVisualStudioVersion -eq "2017") { # [HACK] pch generation crashes on VS 15.5 because of STL library, known bug. # Triggered by addition of line directives to improve std::function debugging. # There's a definition that supresses line directives. $preprocessorDefinitions += "-D_DEBUG_FUNCTIONAL_MACHINERY" } Write-Verbose-Array -array $preprocessorDefinitions -name "Preprocessor definitions" #----------------------------------------------------------------------------------------------- # DETECT PROJECT ADDITIONAL INCLUDE DIRECTORIES AND CONSTRUCT INCLUDE PATHS [string[]] $additionalIncludeDirectories = Get-ProjectAdditionalIncludes Write-Verbose-Array -array $additionalIncludeDirectories -name "Additional include directories" [string[]] $includeDirectories = Get-ProjectIncludeDirectories Write-Verbose-Array -array $includeDirectories -name "Include directories" #----------------------------------------------------------------------------------------------- # LOCATE STDAFX.H DIRECTORY [string] $stdafxCpp = Get-Project-PchCpp [string] $stdafxDir = "" [string] $stdafxHeader = "" [string] $stdafxHeaderFullPath = "" if (![string]::IsNullOrEmpty($stdafxCpp)) { Write-Verbose "PCH cpp name: $stdafxCpp" if ($forceIncludeFiles.Count -gt 0) { $stdafxHeader = $forceIncludeFiles[0] } if (!$stdafxHeader) { $stdafxHeader = Get-PchCppIncludeHeader -pchCppFile $stdafxCpp } if (!$stdafxHeader) { $pchNode = Select-ProjectNodes "//ns:ClCompile[@Include='$stdafxCpp']/ns:PrecompiledHeaderFile" if ($pchNode) { $stdafxHeader = $pchNode.InnerText } } Write-Verbose "PCH header name: $stdafxHeader" $stdafxDir = Get-ProjectStdafxDir -pchHeaderName $stdafxHeader ` -includeDirectories $includeDirectories ` -additionalIncludeDirectories $additionalIncludeDirectories } if ([string]::IsNullOrEmpty($stdafxDir)) { Write-Verbose ("PCH not enabled for this project!") } else { Write-Verbose ("PCH directory: $stdafxDir") $includeDirectories = @(Remove-PathTrailingSlash -path $stdafxDir) + $includeDirectories $stdafxHeaderFullPath = Canonize-Path -base $stdafxDir -child $stdafxHeader -ignoreErrors } #----------------------------------------------------------------------------------------------- # FIND LIST OF CPPs TO PROCESS [System.Collections.Hashtable] $projCpps = @{} foreach ($fileToCompileInfo in (Get-ProjectFilesToCompile -pchCppName $stdafxCpp)) { if ($fileToCompileInfo.File) { $projCpps[$fileToCompileInfo.File] = $fileToCompileInfo } } if ($projCpps.Count -gt 0 -and $aCppToCompile.Count -gt 0) { [System.Collections.Hashtable] $filteredCpps = @{} [bool] $dirtyStdafx = $false foreach ($cpp in $aCppToCompile) { if ($cpp -ieq $stdafxHeaderFullPath) { # stdafx modified => compile all $dirtyStdafx = $true break } if (![string]::IsNullOrEmpty($cpp)) { if ([System.IO.Path]::IsPathRooted($cpp)) { if ($projCpps.ContainsKey($cpp)) { # really fast, use cache $filteredCpps[$cpp] = $projCpps[$cpp] } } else { # take the slow road and check if it matches $projCpps.Keys | Where-Object { IsFileMatchingName -filePath $_ -matchName $cpp } | ` ForEach-Object { $filteredCpps[$_] = $true } } } } if (!$dirtyStdafx) { $projCpps = $filteredCpps } else { Write-Verbose "PCH header has been targeted as dirty. Building entire project" } } Write-Verbose ("Processing " + $projCpps.Count + " cpps") #----------------------------------------------------------------------------------------------- # CREATE PCH IF NEED BE, ONLY FOR TWO CPPS OR MORE [string] $pchFilePath = "" if ($projCpps.Keys.Count -ge 2 -and ![string]::IsNullOrEmpty($stdafxDir)) { # COMPILE PCH Write-Verbose "Generating PCH..." $pchFilePath = Generate-Pch -stdafxDir $stdafxDir ` -stdafxHeaderName $stdafxHeader ` -preprocessorDefinitions $preprocessorDefinitions ` -includeDirectories $includeDirectories ` -additionalIncludeDirectories $additionalIncludeDirectories Write-Verbose "PCH: $pchFilePath" if ([string]::IsNullOrEmpty($pchFilePath) -and $aContinueOnError) { Write-Output "Skipping project. Reason: cannot create PCH." return } } #----------------------------------------------------------------------------------------------- # PROCESS CPP FILES. CONSTRUCT COMMAND LINE JOBS TO BE INVOKED $clangJobs = @() foreach ($cpp in $projCpps.Keys) { [string] $exeToCall = Get-ExeToCall -workloadType $workloadType [string] $finalPchPath = $pchFilePath if ($projCpps[$cpp].Pch -eq [UsePch]::NotUsing) { $finalPchPath = "" Write-Verbose "`n[PCH] Will ignore precompiled headers for $cpp`n" } [string] $exeArgs = Get-ExeCallArguments -workloadType $workloadType ` -pchFilePath $finalPchPath ` -preprocessorDefinitions $preprocessorDefinitions ` -forceIncludeFiles $forceIncludeFiles ` -currentFile $cpp ` -includeDirectories $includeDirectories ` -additionalIncludeDirectories $additionalIncludeDirectories $newJob = New-Object PsObject -Prop @{ 'FilePath' = $exeToCall; 'WorkingDirectory'= Get-SourceDirectory; 'ArgumentList' = $exeArgs; 'File' = $cpp } $clangJobs += $newJob } #----------------------------------------------------------------------------------------------- # PRINT DIAGNOSTICS if ($clangJobs.Count -ge 1) { Write-Verbose "INVOKE: ""$($global:llvmLocation)\$exeToCall"" $($clangJobs[0].ArgumentList)" } #----------------------------------------------------------------------------------------------- # RUN CLANG JOBS Run-ClangJobs -clangJobs $clangJobs } #------------------------------------------------------------------------------------------------- # Script entry point Clear-Host # clears console #------------------------------------------------------------------------------------------------- # If we didn't get a location to run CPT at, use the current working directory if (!$aSolutionsPath) { $aSolutionsPath = Get-Location } # ------------------------------------------------------------------------------------------------ # Load param values from configuration file (if exists) Update-ParametersFromConfigFile #------------------------------------------------------------------------------------------------- # Print script parameters Print-InvocationArguments #------------------------------------------------------------------------------------------------- # Script entry point Write-Verbose "CPU logical core count: $kLogicalCoreCount" # If LLVM is not in PATH try to detect it automatically if (! (Exists-Command($kClangCompiler)) ) { foreach ($locationLLVM in $kLLVMInstallLocations) { if (Test-Path $locationLLVM) { Write-Verbose "LLVM location: $locationLLVM" $env:Path += ";$locationLLVM" $global:llvmLocation = $locationLLVM break } } } Push-Location (Get-SourceDirectory) # fetch .sln paths and data Load-Solutions # This PowerShell process may already have completed jobs. Discard them. Remove-Job -State Completed Write-Verbose "Source directory: $(Get-SourceDirectory)" Write-Verbose "Scanning for project files" [System.IO.FileInfo[]] $projects = Get-Projects [int] $initialProjectCount = $projects.Count Write-Verbose ("Found $($projects.Count) projects") # ------------------------------------------------------------------------------------------------ # If we get headers in the -file arg we have to detect CPPs that include that header if ($aCppToCompile.Count -gt 0) { # We've been given particular files to compile. If headers are among them # we'll find all source files that include them and tag them for processing. [string[]] $headerRefs = Get-HeaderReferences -files $aCppToCompile if ($headerRefs.Count -gt 0) { Write-Verbose-Array -name "Detected source files" -array $headerRefs $aCppToCompile += $headerRefs } } # ------------------------------------------------------------------------------------------------ [System.IO.FileInfo[]] $projectsToProcess = @() [System.IO.FileInfo[]] $ignoredProjects = @() if (!$aVcxprojToCompile -and !$aVcxprojToIgnore) { $projectsToProcess = $projects # we process all projects } else { # some filtering has to be done if ($aVcxprojToCompile) { $projects = $projects | Where-Object { Should-CompileProject -vcxprojPath $_.FullName } $projectsToProcess = $projects } if ($aVcxprojToIgnore) { $projectsToProcess = $projects | ` Where-Object { !(Should-IgnoreProject -vcxprojPath $_.FullName ) } $ignoredProjects = ($projects | Where-Object { $projectsToProcess -notcontains $_ }) } } if ($projectsToProcess.Count -eq 0) { Write-Err "Cannot find given project(s)" } if ($aCppToCompile -and $projectsToProcess.Count -gt 1) { # We've been given particular files to compile, we can narrow down # the projects to be processed (those that include any of the particular files) # For obvious performance reasons, no filtering is done when there's only one project to process. [System.IO.FileInfo[]] $projectsThatIncludeFiles = Get-SourceCodeIncludeProjects -projectPool $projectsToProcess ` -files $aCppToCompile Write-Verbose-Array -name "Detected projects" -array $projectsThatIncludeFiles # some projects include files using wildcards, we won't match anything in them # so when matching nothing we don't do filtering at all if ($projectsThatIncludeFiles) { $projectsToProcess = $projectsThatIncludeFiles } } if ($projectsToProcess.Count -eq $initialProjectCount) { Write-Verbose "PROCESSING ALL PROJECTS" } else { #Write-Output ("PROJECTS: `n`t" + ($projectsToProcess -join "`n`t")) Write-Array -name "PROJECTS" -array $projectsToProcess if ($ignoredProjects) { Write-Array -name "IGNORED PROJECTS" -array $ignoredProjects } } # ------------------------------------------------------------------------------------------------ $projectCounter = $projectsToProcess.Length; foreach ($project in $projectsToProcess) { [string] $vcxprojPath = $project.FullName; [WorkloadType] $workloadType = [WorkloadType]::Compile if (![string]::IsNullOrEmpty($aTidyFlags)) { $workloadType = [WorkloadType]::Tidy } if (![string]::IsNullOrEmpty($aTidyFixFlags)) { $workloadType = [WorkloadType]::TidyFix } Write-Output ("PROJECT$(if ($projectCounter -gt 1) { " #$projectCounter" } else { } ): " + $vcxprojPath) Process-Project -vcxprojPath $vcxprojPath -workloadType $workloadType Write-Output "" # empty line separator $projectCounter -= 1 } if ($global:FoundErrors) { Fail-Script } else { Exit-Script }