Search code examples
.netmsbuildnuget

Insert a Directory.Build target into project dependency pipeline


I have projects spread across two solutions, call them Mine.sln and Theirs.sln (and they live in separate repositories). The projects in Theirs are all dependencies for Mine. The two solutions are currently built in sequence. I would like to change this so that the dependencies are built as a part of Mine, eliminating the need to work with two solutions (which are also in separate repositories). The problem is that in Theirs, the projects have HintPath properties set on nuget package references (which are pre-sdk projects, unfortunately).

To make this a bit more concrete:

Mine.sln (14 projects)
 +=> MP0
 +=> MP1
 +=> ...
 +=> MP13
Theirs.sln (21 projects)
 +=> TP0
 +=> TP1
 +=> ...
 +=> TP20

I have added about 10 or so of the projects from Theirs to Mine, and there are project dependencies like MP0 => TP1, TP2, etc.

The problem: when Theirs are built as dependencies from Mine, those HintPath properties, which are relative to the Theirs solution, are no longer correct and the dependency projects fail to build. There is a GitHub issue dealing with this problem, although there isn't really a solution: https://github.com/NuGet/Home/issues/738

Modifying projects in Theirs isn't really an option, so I was exploring the possibility of doing something like rewriting those properties in memory during the build process. There is a project to do this on a project-by-project basis, and I've looked at the targets file they use (https://github.com/jstangroome/NuGetReferenceHintPathRewrite/blob/master/content/NuGetReferenceHintPathRewrite.targets), but it's per-project and would require modifying projects in Theirs, which again, isn't really an option.

My hope was to use Directory.Build.targets at the root of Mine.sln to set up a target that would run before ResolveAssemblyReferences for every project built by this solution. My Directory.Build.targets file looks like this:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="ReferenceHintPathUpdate" BeforeTargets="BeforeResolveAssemblyReference">
        <Message Text="ReferenceHintPathUpdate: Building $(MSBuildProjectName) in $(SolutionDir)" Importance="High" />
    </Target>
</Project>

I only have a Message task here to test out build order and dependencies. I was expecting to see every project name output in the terminal, but I only get output from some projects (some referenced by Mine, and a few referenced by Theirs, but nothing like all of them). I haven't yet been able to determine why this target only runs for some projects; there doesn't seem to be an obvious common pattern.

Example (filtered) output:

ReferenceHintPathUpdate: Building MP0 in C:\Users\Me\MySolution
ReferenceHintPathUpdate: Building MP1 in C:\Users\Me\MySolution
...
ReferenceHintPathUpdate: Building MP7 in C:\Users\Me\MySolution
ReferenceHintPathUpdate: Building TP0 in C:\Users\Me\MySolution
ReferenceHintPathUpdate: Building TP4 in C:\Users\Me\MySolution

What do I need to do to make a target defined in Directory.Build.targets run for every single project built by this solution, especially the dependency projects from Theirs.sln?


Solution

  • Migrate the projects (or as many as possible) away from using packages.config and to using PackageReference. PackageReference can be used in both legacy style projects and SDK style projects. If you have any .Net Framework ASP.NET projects, those projects can't be migrated and need to continue to use packages.config.

    With PackageReference there is no hint path.

    How are you solving the issue of the two repos? Is the version control system git and is Theirs pulled in as a submodule in a subdirectory?

    I'll assume you can't map or move the TP* project directories so that the relative HintPath works. (There are some version control systems that support 'overlaying' repos but git submodules don't work that way.)

    I'll assume there are projects that can't be changed to PackageReference either because the project is .Net Framework ASP.NET or because is it Theirs.

    For both legacy style and SDK style projects the Directory.Build.props and Directory.Build.targets files are automatically imported if found. MSBuild searches up the parent directories to find the files. See Customize the build by folder and note the section on Search scope.

    I find it extremely useful to 'chain' the Directory.Build.* files but that has to be explicitly added to each file. See the section on multi-level merging.

    If the Theirs repo is using Directory.Build.targets files without chaining, your Directory.Build.targets file will not be imported. I expect this is the issue when different projects seem to have different behavior.

    The following can be added to all Directory.Build.props and Directory.Build.targets files.

    <Import Project="$([MSBuild]::GetPathOfFileAbove('$(MSBuildThisFile)', '$(MSBuildThisFileDirectory)../'))" Condition="'$([MSBuild]::GetPathOfFileAbove('$(MSBuildThisFile)', '$(MSBuildThisFileDirectory)../'))' != ''" />
    

    This is a variant of the example Import in multi-level merging.

    Instead of hard-coding the file name, the $(MSBuildThisFile) property is used.

    If a file is not found, GetPathOfFileAbove returns an empty string. It is important to have the Condition because Import will cause an error when the Project attribute evaluates to an empty string. With the Condition you don't need to be concerned about which file is highest in the directory structure. It also means the chain is extensible at the top.

    I like to consistently have this Import at the beginning of a Directory.Build.* file. If the Import is in different places in different files, the chaining is hard to understand and troubleshoot. Also putting the import at the beginning of a file means the current file can use and/or override things from the imported file.

    It would probably be best if you can make the case for a few changes in Theirs: use PackageReference and add chaining for Directory.Build.* files.