mark
mark

Reputation: 62746

How to code a utility msbuild project so that it depends on a "real" C# project?

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:

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.

Upvotes: 1

Views: 301

Answers (1)

stijn
stijn

Reputation: 35901

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>

Upvotes: 1

Related Questions