Skip to content

CPT Visual Studio project file parsing does not respect .props/.targets file conventions #1412

@jtafarrelly

Description

@jtafarrelly

I encountered this issue whilst trying to get the clang-build.ps1 and associated scripts working in a CI environment for static analysis, with a visual studio solution containing multiple project files that uses VCPKG for dependency management, and various .props and .targets files to handle shared settings between project files.

TL;DR CPT has incorrect parsing behaviour on the .vcxproj XML elements for imported property sheets, using a hacky workaround that causes certain standard files to be loaded in the incorrect order, or not loaded at all. This causes all sorts of hard-to-debug havoc in the resulting environment due to variables not being set correctly - for example, the settings provided to vcpkg are loaded before a project specifies to use VCPKG manifest mode, leading to a whole raft of paths being incorrect.

Inspecting the internal parsing logic inside msbuild-project-load.ps1, we can see that it's incredibly funky.

To start, when parsing a Project.vcxproj XML document, the script will only attempt to find a Directory.Build.props file if it cannot find \Microsoft.Cpp.props :

    {
        [string] $relPath = Evaluate-MSBuildExpression $node.GetAttribute("Project")
        if (!$relPath)
        {
            return
        }
        [string[]] $paths = @(Canonize-Path -base (Get-Location) -child $relPath -ignoreErrors)

        [bool] $loadedProjectSheet = $false
        foreach ($path in $paths)
        {
            if (![string]::IsNullOrEmpty($path) -and (Test-Path -LiteralPath $path))
            {
                Write-Verbose "Property sheet: $path"
                ParseProjectFile($path)
                $loadedProjectSheet = $true
            }
        }
        if (!$loadedProjectSheet)
        {
            Write-Verbose "Could not find property sheet $relPath"
            if ($relPath -like "\Microsoft.Cpp.props")
            {
                # now we can begin to evaluate directory.build.props XML element conditions, load it
                LoadDirectoryBuildPropSheetFile
            }
        }
    }

This causes a number of other issues, like causing different script behaviour when run from a Visual Studio Developer Powershell instance (this had me tearing my hair out!). Inspecting the LoadDirectoryBuildPropSheetFile function, and the issue gets worse:

{
    if ($env:CPT_LOAD_ALL -ne "1")
    {
        # Tries to find a Directory.Build.props property sheet, starting from the
        # project directory, going up. When one is found, the search stops.
        # Multiple Directory.Build.props sheets are not supported.
        [string] $directoryBuildSheetPath = (cpt::GetDirNameOfFileAbove -startDir $ProjectDir `
                                             -targetFile "Directory.Build.props") + "\Directory.Build.props"
        if (Test-Path -LiteralPath $directoryBuildSheetPath)
        {
            ParseProjectFile($directoryBuildSheetPath)
        }

        [string] $vcpkgIncludePath = "$env:LOCALAPPDATA\vcpkg\vcpkg.user.props"
        if (Test-Path -LiteralPath $vcpkgIncludePath)
        {
            ParseProjectFile($vcpkgIncludePath)
        }
        [string] $vcpkgIncludePath = "$env:LOCALAPPDATA\vcpkg\vcpkg.user.targets"
        if (Test-Path -LiteralPath $vcpkgIncludePath)
        {
            ParseProjectFile($vcpkgIncludePath)
        }
    }
}

This loads the vcpkg.user.targets file (and thus vcpkg.targets) immediately after, before any intermediate properties sheets or per-project file settings are configured, which the vcpkg.targets depends on. This would normally be loaded by Microsoft.cpp.targets, which is imported at the very end of the .vcxproj file. Moreover, the parsing logic completely skips handling Directory.Build.targets!

From https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory?view=vs-2022, Directory.Build.Props is imported before basically every other property sheet, including Microsoft.Cpp.props:

Directory.Build.props is imported early in Microsoft.Common.props, and properties defined later are unavailable to it. So, avoid referring to properties that aren't yet defined (and will evaluate to empty).

Conversely, Directory.Build.targets is loaded late in the process, after 'Microsoft.Cpp.targets`:

Directory.Build.targets is imported from Microsoft.Common.targets after importing .targets files from NuGet packages. So, it can override properties and targets defined in most of the build logic, or set properties for all your projects regardless of what the individual projects set.

Suggested remediation:

  1. Automatically detect the location of Microsoft.cpp.props, Microsoft.cpp.targets and similarly imported common property sheets used by Visual Studio projects/solutions, ensuring any dependent variables are appropriately and consistently defined.

By default, these are located at $(VCTargetsPath)\, defined using the property sheets set in the MSBuild installation folder and various subfolders. (eg, on my development machine, this would be C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild - for CI scenarios you may have a standalone copy of MSBuild, invoking clang-build.ps1 via a terminal.)

  1. Failing the above, add a separate parsing section for \Microsoft.cpp.targets (loading vcpkg.user.targets) and Directory.Build.targets, at the appropriate point in the .vcxproj parsing logic.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions