Error building custom target in same directory as project with NuGet references VS2022 generator
This is a bit of a weird issue that might end up actually being considered a pure MSBuild issue, but I'm filing it here to start with for those familiar with CMake's inner workings to decide whether a fix should be implemented in cmake or in MSBuild.
Take the following simple CMakeLists.txt file:
cmake_minimum_required(VERSION 3.28.0)
project(test_project LANGUAGES CSharp)
add_library(lib SHARED test.cs)
set_target_properties(lib PROPERTIES
VS_PACKAGE_REFERENCES "MSTest.TestFramework_2.2.10")
add_custom_target(custom ALL DEPENDS lib)
(The contents of test.cs are irrelevant - an empty file will do)
If cmake is pointed at this file using the VS2022 generator, and then Visual Studio (or MSBuild directly) is used to build the custom
project, the following error is generated:
Build started at 12:16...
1>------ Build started: Project: ZERO_CHECK, Configuration: Debug x64 ------
Restored C:\Work\TempWork\cmake-experiment\build\lib.csproj (in 44 ms).
1>1>Checking Build System
2>------ Build started: Project: lib, Configuration: Debug x64 ------
2> lib -> C:\Work\TempWork\cmake-experiment\build\Debug\lib.dll
3>------ Build started: Project: custom, Configuration: Debug x64 ------
3>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Microsoft\NuGet\17.0\Microsoft.NuGet.targets(198,5): error : Sequence contains no elements
3>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Microsoft\NuGet\17.0\Microsoft.NuGet.targets(198,5): error : at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
3>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Microsoft\NuGet\17.0\Microsoft.NuGet.targets(198,5): error : at Microsoft.NuGet.Build.Tasks.ResolveNuGetPackageAssets.GiveErrorForMissingFramework()
3>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Microsoft\NuGet\17.0\Microsoft.NuGet.targets(198,5): error : at Microsoft.NuGet.Build.Tasks.ResolveNuGetPackageAssets.GetTargetOrAttemptFallback(JObject lockFile, Boolean needsRuntimeIdentifier)
3>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Microsoft\NuGet\17.0\Microsoft.NuGet.targets(198,5): error : at Microsoft.NuGet.Build.Tasks.ResolveNuGetPackageAssets.GetReferences(JObject lockFile)
3>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Microsoft\NuGet\17.0\Microsoft.NuGet.targets(198,5): error : at Microsoft.NuGet.Build.Tasks.ResolveNuGetPackageAssets.ExecuteCore()
3>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Microsoft\NuGet\17.0\Microsoft.NuGet.targets(198,5): error : at Microsoft.NuGet.Build.Tasks.ResolveNuGetPackageAssets.Execute()
3>Done building project "custom.vcxproj" -- FAILED.
4>------ Skipped Build: Project: ALL_BUILD, Configuration: Debug x64 ------
4>Project not selected to build for this solution configuration
========== Build: 2 succeeded, 1 failed, 0 up-to-date, 1 skipped ==========
========== Build completed at 12:16 and took 01.268 seconds ==========
For reference, I've seen this with a range of CMake versions, up to and including the latest (3.28), and using VS2022 update 17.8.3 (it's likely present in older versions too, but I haven't checked) This issue is not present when using VS2019 or older generators (see below for analysis).
A workaround is to put the custom
target in a different directory.
To try and identify the issue, I dug into what MSBuild is doing when building the empty custom target (NB: The fact that it is empty is irrelevant to the issue - in the real case, the target is executing a custom command, but it never gets that far).
For reference, the exception is raised because this line of code is being executed by the build: https://github.com/dotnet/NuGet.BuildTasks/blob/a5309cd95cce99b055db528f95939c76dbd47903/src/Microsoft.NuGet.Build.Tasks/ResolveNuGetPackageAssets.cs#L782C13-L782C48. The exception is because the code is assuming a non-empty sequence is being inputted. I looked into how this happened, and discovered the following:
- The
ResolveNuGetPackageAssets
task is failing because it requires a non-empty string in one of its properties. It is called at line 198 of Microsoft.NuGet.targets, available within the VS2022 installation. - This bad property is set, ultimately, only when the
TargetRuntime
property is set toManaged
. - Prior to VS2022, this property was always set to
Managed
for some reason, unless explicitly overridden. This looks to be a bug in the default target files that was fixed with VS2022 (even native C++ projects had a value of Managed for this field prior to VS2022). - When the
TargetRuntime
property is set toNative
, theTargetMonikers
property is left unset, ultimately resulting in the empty string that results in the exception raised in point 1. - This doesn't impact most
Native
projects because theResolveNuGetPackageAssets
target (which calls the task of the same name) is skipped. It is skipped if there is no file atobj\project.assets.json
relative to the project file. This file appears to be generated when NuGet does a package restore, which happens by default at the start of a build of a project that relies on pacakges from NuGet. - The
custom.vcxproj
file is located in the same directory aslib.csproj
in the above example and requireslib.csproj
to be built first. - Since the
lib
target relies on the MSTest (in this case) NuGet package, NuGet generates aobj\project.assets.json
. - Since the relative path is the same, MSBuild sees the
obj\project.assets.json
file forcsharp_api_tests
libwhen doing the
custom` project so thinks it should run the ResolveNuGetPackageAssets target, which then fails as described.
I think arguably there are two bugs, one in CMake and one in MSBuild:
- MSBuild probably shouldn't assume that there is only one project in a directory and therefore that any
obj\project.assets.json
file belongs to that project. (On the other hand, if MSBuild has the assumption that projects are never in the same directory as each other, CMake should probably adhere to it and generate the project files in a manner that doesn't violate this assumption, or at least prevent users from doing so). - CMake should generate a project file that doesn't result in the
ResolveNuGetPackageAssets
target being executed. I note that this target is not executed if the project file has a.xproj
extension. I don't know specifically what this extension is supposed to mean (if I rename a file, the icon changes to a C# one for what it's worth), but perhaps it could be used in some manner. More generally, CMake should probably be generating project files with a language agnostic extension, rather than .vcxproj, when generating the project fromadd_custom_target
, as these are not C++ targets.