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
?
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.