1

By utility I mean a project that does not have any C# files, does not produce a .NET assembly, but implements some custom build logic.

I could have arranged it as an AfterBuild target in the C# project of interest, but I do not want to increase the build time of that C# project. Instead, I want msbuild to run this logic in parallel with other dependents of that C# project.

One solution would be to create a dummy C# project that would truly build some dummy code and put my logic in the AfterBuild target. But that is ugly.

So, here is my solution (Spoiler Alert - it does not work):

Directory structure

C:\work\u [master]> tree /F
Folder PATH listing for volume OSDisk
Volume serial number is F6C4-7BEF
C:.
│   .gitignore
│   Deployer.sln
│
├───Deployer
│       Deployer.csproj
│
├───DeploymentEngine
│       DeploymentEngine.csproj
│
└───Utility
        Utility.csproj

C:\work\u [master]>

Deployer.csproj

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
    <ProjectGuid>{B451936B-54B7-41D1-A359-4B06865248CE}</ProjectGuid>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <OutputType>Library</OutputType>
    <BaseOutputPath>bin</BaseOutputPath>
    <PlatformTarget>AnyCPU</PlatformTarget>
    <ErrorReport>prompt</ErrorReport>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
    <DefineConstants>DEBUG;TRACE</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\DeploymentEngine\DeploymentEngine.csproj">
      <Project>{901487BE-C604-4251-8485-3E96D5993145}</Project>
      <Name>DeploymentEngine</Name>
    </ProjectReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <Target Name="TakeTime" AfterTargets="Build">
    <Exec Command="powershell -NoProfile -Command Start-Sleep -Seconds 5" />
  </Target>
</Project>

Yes, it is a legacy style project because the real solution is a mix of legacy and SDK style projects.

DeploymentEngine.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
  </PropertyGroup>
  <Target Name="TakeTime" AfterTargets="Build">
    <Exec Command="powershell -NoProfile -Command Start-Sleep -Seconds 5" />
  </Target>
</Project>

Utility.csproj

<Project>
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
    <EnableDefaultItems>False</EnableDefaultItems>
    <GenerateAssemblyInfo>False</GenerateAssemblyInfo>
  </PropertyGroup>
  <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
  <Target Name="Build">
    <Message Text="*** Good" Importance="high" Condition="Exists('..\DeploymentEngine\bin\Debug\net472\DeploymentEngine.dll')" />
    <Message Text="*** Bad" Importance="high" Condition="!Exists('..\DeploymentEngine\bin\Debug\net472\DeploymentEngine.dll')" />
  </Target>
  <Target Name="Clean" />
  <Target Name="Rebuild" DependsOnTargets="Clean;Build" />
  <ItemGroup>
    <ProjectReference Include="..\DeploymentEngine\DeploymentEngine.csproj" />
  </ItemGroup>
</Project>

Deployer.sln

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31205.134
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deployer", "Deployer\Deployer.csproj", "{B451936B-54B7-41D1-A359-4B06865248CE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeploymentEngine", "DeploymentEngine\DeploymentEngine.csproj", "{901487BE-C604-4251-8485-3E96D5993145}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Utility", "Utility\Utility.csproj", "{9369D18D-D81D-4CA3-A287-C62C89BFB751}"
EndProject
Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Debug|Any CPU = Debug|Any CPU
                Release|Any CPU = Release|Any CPU
        EndGlobalSection
        GlobalSection(ProjectConfigurationPlatforms) = postSolution
                {B451936B-54B7-41D1-A359-4B06865248CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
                {B451936B-54B7-41D1-A359-4B06865248CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
                {B451936B-54B7-41D1-A359-4B06865248CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
                {B451936B-54B7-41D1-A359-4B06865248CE}.Release|Any CPU.Build.0 = Release|Any CPU
                {901487BE-C604-4251-8485-3E96D5993145}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
                {901487BE-C604-4251-8485-3E96D5993145}.Debug|Any CPU.Build.0 = Debug|Any CPU
                {901487BE-C604-4251-8485-3E96D5993145}.Release|Any CPU.ActiveCfg = Release|Any CPU
                {901487BE-C604-4251-8485-3E96D5993145}.Release|Any CPU.Build.0 = Release|Any CPU
                {9369D18D-D81D-4CA3-A287-C62C89BFB751}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
                {9369D18D-D81D-4CA3-A287-C62C89BFB751}.Debug|Any CPU.Build.0 = Debug|Any CPU
                {9369D18D-D81D-4CA3-A287-C62C89BFB751}.Release|Any CPU.ActiveCfg = Release|Any CPU
                {9369D18D-D81D-4CA3-A287-C62C89BFB751}.Release|Any CPU.Build.0 = Release|Any CPU
        EndGlobalSection
        GlobalSection(SolutionProperties) = preSolution
                HideSolutionNode = FALSE
        EndGlobalSection
        GlobalSection(ExtensibilityGlobals) = postSolution
                SolutionGuid = {A70FF6AB-85B1-49F0-B2B0-25E20256A88F}
        EndGlobalSection
EndGlobal

Notes:

  • I placed an artificial delay into the two "real" C# projects.
  • The Utility project outputs *** Bad when it is run NOT after its declared dependency, i.e. NOT after the DeploymentEngine project.

Now let us run it:

C:\work\u [master]> git clean -qdfx ; msbuild /v:m /restore /m
Microsoft (R) Build Engine version 16.11.0+0538acc04 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored C:\work\u\DeploymentEngine\DeploymentEngine.csproj (in 171 ms).
  Restored C:\work\u\Utility\Utility.csproj (in 172 ms).
  *** Bad
  DeploymentEngine -> C:\work\u\DeploymentEngine\bin\Debug\net472\DeploymentEngine.dll
CSC : warning CS2008: No source files specified. [C:\work\u\Deployer\Deployer.csproj]
  Deployer -> C:\work\u\Deployer\bin\Debug\Deployer.dll
C:\work\u [master]>

The output indicates the Utility project was built first, despite the declared intent of depending on the DeploymentEngine project.

Notice, if I run the build single threaded the output will be *** Good, so the output logic does work correctly:

C:\work\u [master]> git clean -qdfx ; msbuild /v:m /restore
Microsoft (R) Build Engine version 16.11.0+0538acc04 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored C:\work\u\Utility\Utility.csproj (in 172 ms).
  Restored C:\work\u\DeploymentEngine\DeploymentEngine.csproj (in 172 ms).
  DeploymentEngine -> C:\work\u\DeploymentEngine\bin\Debug\net472\DeploymentEngine.dll
CSC : warning CS2008: No source files specified. [C:\work\u\Deployer\Deployer.csproj]
  Deployer -> C:\work\u\Deployer\bin\Debug\Deployer.dll
  *** Good
C:\work\u [master]>

So just declaring ProjectReference is not enough. Seems like I should implement some kind of a target to make it work.

So what am I missing? What should I add to let msbuild know that the Utility project must be built after the DeploymentEngine ?

EDIT 1

I know I can set dependencies in the solution file. However, I do not want to do it for various reasons.

EDIT 2

My ultimate goal is to have a bare bones utility project that runs after one or more "real" C# projects. I.e. as few .NET build imports as possible. And if it could have the .proj extension, rather than .csproj - the best.

mark
  • 59,016
  • 79
  • 296
  • 580
  • Did you try to set the project dependencies at solution level in VS (right-click solution->Project Dependencies)? – stijn Dec 02 '21 at 07:12
  • I forgot to mention that I do not want to do it. Let me update the post. – mark Dec 02 '21 at 15:43
  • Did you create the solution manually? If I add a project like yours to the solution, it seems to automatically figure out that ProjectReference means dependency and shows it as such in the Project Dependencies. And consequently things work with correct build order. Moreover removing that dependency is not allowed: "This dependency was added by the project system and cannot be removed" (on the other hand there's also no trace from it in the .sln, so really seems to just be ProjectReference which takes care of it). tldr; cannot reproduce also not on commandline. Provide a minimal sample? – stijn Dec 02 '21 at 16:55
  • @stijn - I have updated the question with the minimal reproduction. Thank you very much. – mark Dec 03 '21 at 18:35
  • Ok, I missed the `-m` earlier, that indeed introduces the problem – stijn Dec 04 '21 at 14:38

1 Answers1

1

I was wondering why you didn't use <Project Sdk="Microsoft.Net.Sdk"/> for the utility project and reading the docs that's because it would implicitly import Sdk.targets at the end of everything else, thereby overriding your Build target.

I haven't figured how exactly yet (don't have more time now, but I'm pretty sure that it should be possible to have a more bare bones project and still have ProjectReference functioning properly - will be a matter of declaring the correct properties and targets; which might end up being more work than just hacking around in the existing structure though), but that target is key to making msbuild respect the ProjectReference and maintain correct build order: among other things it depends on ResolveProjectReferences which is the target responsible for actually dealing with ProjectReference. Msbuild itself doesn't know anything about those, the logic for that is supplied by Microsoft.Common.CurrentVersion.targets.

As such simply overriding the Build target will make ProjectReference being ignored completely. The sole reason the solution does build in the wanted order when not using -m is that the utility project comes last. If you'de move it up in the .sln, msbuild will build it earlier and it will print '*** Bad'.

First attempt: Build does a lot so I figured leveraging it just for what you need and leaving it intact for the rest should do it. Not super clean, but does the job:

<Project>
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
    <GenerateAssemblyInfo>False</GenerateAssemblyInfo>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\DeploymentEngine\DeploymentEngine.csproj">
    </ProjectReference>
  </ItemGroup>
  <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
  <!-- Override Compile instead of Build, thereby also skipping
        creating of Utility.dll -->
  <Target Name="Compile">
    <Message Text="*** Good" Importance="high" Condition="Exists('..\DeploymentEngine\bin\Debug\net472\DeploymentEngine.dll')" />
    <Message Text="*** Bad" Importance="high" Condition="!Exists('..\DeploymentEngine\bin\Debug\net472\DeploymentEngine.dll')" />
  </Target>
  <!-- Empty so it doesn't try to copy the nonexisting Utility.dll. -->
  <Target Name="CopyFilesToOutputDirectory" />
</Project>

Second attempt: the first attempt uses Sdk.targets etc which basically is saying "I'm a full .Net project", hence the hacky workaround. Simpler is to use only what is in CurrentVersion.Targets i.e. the ResolveProjectReferences target to make the ProjectReference work, so being close to a 'true' utility project (name it Utility.proj):

<Project>
  <PropertyGroup>
    <!-- ProjectReference requires the referenced project to have the same version -->
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <!-- Required for Microsoft.Common.CurrentVersion.targets -->
    <OutputPath>bin</OutputPath>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\DeploymentEngine\DeploymentEngine.csproj">
    </ProjectReference>
  </ItemGroup>
  <!-- For ResolveProjectReferences and everything it does -->
  <Import Project="$(MSBuildBinPath)\Microsoft.Common.CurrentVersion.targets"/>
  <Target Name="Build" DependsOnTargets="ResolveProjectReferences">
    <Message Text="*** Good" Importance="high" Condition="Exists('..\DeploymentEngine\bin\Debug\net472\DeploymentEngine.dll')" />
    <Warning Text="*** Bad" Condition="!Exists('..\DeploymentEngine\bin\Debug\net472\DeploymentEngine.dll')" />
  </Target>
</Project>
stijn
  • 34,664
  • 13
  • 111
  • 163
  • Could you, please, paste the whole file? – mark Dec 04 '21 at 19:29
  • Sure; basically just replaced Build With Compile and excluded Clean/Rebuild because not needed/relevant here. – stijn Dec 04 '21 at 19:46
  • @mark also see edit with a better version – stijn Dec 05 '21 at 14:32
  • There is one minor problem, because it does not block any developer. But it kind of ugly - Visual Studio refuses to open the Utility project. It is not loaded. Which I guess is a minor nuisance. I guess it could be a new question. – mark Dec 05 '21 at 16:14
  • Right, didn't check that. However even if the import(s) which VS wants would be there, it might still complain about other things missing like a Configuration/Platform not being set etc. So that's a bunch of extra boilerplate with no functionality. But yes if you need it, would be good for a separate question. – stijn Dec 05 '21 at 20:11
  • VS IDE gives me problems with the solution file. It tries to change it by replacing `Debug|Any CPU` with `Debug|x86` which makes msbuild skip the compilation of the Utility projects. I cannot rely on the developers NOT to commit the modified version. Need to find a way to stop VS from doing it. Any advice? – mark Dec 14 '21 at 05:51
  • @mark how do I reproduce that? I mean if I load the .sln in VS it just loads (without utility.proj because I just changed the .csproj to .proj) with Debug/AnyCPU selected and then builds, without changes. – stijn Dec 14 '21 at 17:03
  • I will ask a dedicated question. – mark Dec 15 '21 at 01:03
  • https://stackoverflow.com/questions/70371493/how-to-prevent-visual-studio-from-changing-the-solution-file-in-the-scenario-of – mark Dec 15 '21 at 23:20