Show Blogger Panel Hide Blogger Panel
Alex Yakunin

September 16, 2009

Making MSBuild / Visual Studio to automatically copy all indirect dependencies to "bin" folder

Yesterday I asked this question on StackOverflow.com, and didn't get the answer I wanted. So is it possible to make MSBuild to automatically copy all indirect references (dependencies) to output folder?

Yes, this is possible, and the solution is provided below. But first let's think when this is desirable. Actually I hardly imagine why this does not always happen automatically. Really, if AssemblyA needs AssemblyB, and  my application needs AssemblyA, most likely, it won't work without AssemblyB as well. But as you know, AssemblyB won't be automatically copied to bin folder, if it isn't directly referenced from your project, that is actually a rare case, especially if you tend to use loosely coupled components.

Let's list few particular examples we have:

Case 1. Our SQL DOM project consists of core assembly (Xtensive.Sql) and a set of SQL DOM providers (Xtensive.Sql.Oracle, ...), and its quite desirable to copy all of them to application's bin folder, because generally it can use any provider. Let's think I created Xtensive.Sql.All assembly referencing all of them (btw, I really did this in our repository). Actually, this assembly contains a single type, which will never be instantiated:

  /// <summary>
  /// Does nothing, but references types from all SQL DOM assemblies.
  /// </summary>
  public sealed class Referencer
  {
    private Type[] types = new [] {
      typeof (Pair<>),
      typeof (SqlType),
      typeof (SqlServer.DriverFactory),
      typeof (PostgreSql.DriverFactory),
      typeof (Oracle.DriverFactory),
      typeof (VistaDb.DriverFactory),
    };

    // This is the only constructor. So you can't instantiate this type.
    private Referencer()
    {
    }
  }

As you see, this type references types from all SQL DOM assemblies (including its providers). This is necessary, because otherwise C# complier will not add references to these assemblies to Xtensive.Sql.All.dll, even although the project it is built by includes them.

So practically you can't use this type. But it makes C# compiler to list all the references we need Xtensive.Sql.All.dll assembly:



Note that each of these assemblies also needs many others. For example, let's take a look at Xtensive.Sql.PostgreSql.dll assembly there. It references Npgsql.dll, which in turn references Mono.Security.dll.

So now you understand the problem. I'd like all these assemblies to be in bin folder of my application automatically. I don't want to manually discover all the dependencies and write a code like this to copy them:

  <Target Name="AfterBuild" DependsOnTargets="RequiresPostSharp">
    <CreateItem Include="$(SolutionDir)\Lib\*.*">
      <Output TaskParameter="Include" ItemName="CopyFiles" />
    </CreateItem>
    <Copy SourceFiles="@(CopyFiles)" DestinationFolder="$(TargetDir)" SkipUnchangedFiles="true" />
  </Target>


Case 2. The same is about our Xtensive.Storage providers and assemblies. So I created Xtensive.Storage.All assembly referencing all you might need. This assembly contains very similar Referencer type.

Let's go to the solution now.

Solution: CopyIndirectDependencies.targets.

Here it is:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <CopyIndirectDependencies    
      Condition="'$(CopyIndirectDependencies)'==''">true</CopyIndirectDependencies>
    <CopyIndirectDependenciesPdb 
      Condition="'$(CopyIndirectDependenciesPdb)'==''">false</CopyIndirectDependenciesPdb>
    <CopyIndirectDependenciesXml 
      Condition="'$(CopyIndirectDependenciesXml)'==''">false</CopyIndirectDependenciesXml>
  </PropertyGroup>


  <!-- BuildXxx part -->

  <Target Name="CopyIndirectDependencies" 
          Condition="'$(CopyIndirectDependencies)'=='true'"
          DependsOnTargets="DetectIndirectDependencies">
    <Copy Condition="'%(IndirectDependency.FullPath)'!=''"
          SourceFiles="%(IndirectDependency.FullPath)" 
          DestinationFolder="$(OutputPath)" 
          SkipUnchangedFiles="true" >
      <Output TaskParameter="CopiedFiles" 
              ItemName="IndirectDependencyCopied" />
    </Copy>
    <Message Importance="low"
             Condition="'%(IndirectDependencyCopied.FullPath)'!='' 
               and '%(IndirectDependencyCopied.Extension)'!='.pdb' 
               and '%(IndirectDependencyCopied.Extension)'!='.xml'"
             Text="Indirect dependency copied: %(IndirectDependencyCopied.FullPath)" />
  </Target>

  <Target Name="DetectIndirectDependencies"
          DependsOnTargets="ResolveAssemblyReferences">
    
    <Message Importance="low"
             Text="Direct dependency: %(ReferencePath.Filename)%(ReferencePath.Extension)" />
    <Message Importance="low"
             Text="Indirect dependency: %(ReferenceDependencyPaths.Filename)%(ReferenceDependencyPaths.Extension)" />

    <!-- Creating indirect dependency list -->
    <CreateItem Include="%(ReferenceDependencyPaths.FullPath)" 
                Condition="'%(ReferenceDependencyPaths.CopyLocal)'=='true'">
      <Output TaskParameter="Include" 
              ItemName="_IndirectDependency"/>
    </CreateItem>
    <CreateItem Include="%(ReferenceDependencyPaths.RootDir)%(ReferenceDependencyPaths.Directory)%(ReferenceDependencyPaths.Filename).xml"
                Condition="'%(ReferenceDependencyPaths.CopyLocal)'=='true' and '$(CopyIndirectDependenciesXml)'=='true'">
      <Output TaskParameter="Include" 
              ItemName="_IndirectDependency"/>
    </CreateItem>
    <CreateItem Include="%(ReferenceDependencyPaths.RootDir)%(ReferenceDependencyPaths.Directory)%(ReferenceDependencyPaths.Filename).pdb"
                Condition="'%(ReferenceDependencyPaths.CopyLocal)'=='true' and '$(CopyIndirectDependenciesPdb)'=='true'">
      <Output TaskParameter="Include" 
              ItemName="_IndirectDependency"/>
    </CreateItem>

    <!-- Filtering indirect dependency list by existence -->
    <CreateItem Include="%(_IndirectDependency.FullPath)"
                Condition="Exists('%(_IndirectDependency.FullPath)')">
      <Output TaskParameter="Include" 
              ItemName="IndirectDependency"/>
    </CreateItem>

    <!-- Creating copied indirect dependency list -->
    <CreateItem Include="@(_IndirectDependency->'$(OutputPath)%(Filename)%(Extension)')">
      <Output TaskParameter="Include"
              ItemName="_ExistingIndirectDependency"/>
    </CreateItem>

    <!-- Filtering copied indirect dependency list by existence -->
    <CreateItem Include="%(_ExistingIndirectDependency.FullPath)"
                Condition="Exists('%(_ExistingIndirectDependency.FullPath)')">
      <Output TaskParameter="Include"
              ItemName="ExistingIndirectDependency"/>
    </CreateItem>

  </Target>


  <!-- Build sequence modification -->

  <PropertyGroup>
    <CoreBuildDependsOn>
      $(CoreBuildDependsOn);
      CopyIndirectDependencies
    </CoreBuildDependsOn>
  </PropertyGroup>
</Project>

Its intended usage: add a single highlighted line importing this file to any .csproj / .vbproj.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  ...

  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <Import Project="CopyIndirectDependencies.targets" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

Check out demo application using this .target:
  • Download and extract it
  • Open it in Visual Studio and build it there, or build it by typing "msbuild" (of course, if it is in your PATH)
  • Check out "bin" folder. It already contains Mono.Security.dll from Lib folder, although application references just Npgsql.dll (it requires Mono.Security.dll).
If you'd like to suppress Visual Studio warning on opening such modified projects for the first time, see this article (in particular, "Non-standard Import Elements" section).

Update: initial version of CopyIndirectDependencies.targets published here was buggy, but now it's fixed.

9 comments:

  1. This doesn't work for me : I have neither Npgsql.dll nor Mono.Security in the bin output folder.

    Have I done something wrong? (tested with Visual Studio 2008 and msbuild)

    ReplyDelete
  2. No... That's really strange. I just downloaded the project, extracted it and built it with MSBuild and VS.NET 2008. In both cases both dlls there in bin folder.

    I'll ask someone else to check this and notify you here.

    ReplyDelete
  3. "both dlls there" -> "both dlls were"

    ReplyDelete
  4. Oh, that's really fun: that's because you have DO4 installed. Its installer places these two assemblies into GAC, but I intentionally made this .targets file to skip copying any assemblies from GAC!

    Now imagine, how we found this: none of DO4 developers have these assemblies installed into GAC, since our current version does not require this. But there was Alex Ilyin (LiveUI), that uses official v4.0.5. And fully occasionally he decided to test this first. That was a huge luck ;)

    ReplyDelete
  5. So .targets file works. You can test it e.g. after running GAC/GacUninstall.bat file (there is GacInstall.bat as well, so it's safe).

    ReplyDelete
  6. Update: initial version of CopyIndirectDependencies.targets published here was buggy, but now it's fixed.

    ReplyDelete
  7. This almost works. For some reason, it's detecting one of my DLLs as "CopyLocal='false'" even though the "True" is set explicitly in the other project.

    If I change all the conditions to use Private=true, I get all sorts of extra indirect references I don't want...

    ReplyDelete
  8. Anonymous05:11

    Having a problem with grabbing indirect dlls referenced from a packages folder of nuget. Any way to supply additional directories to get the reference dependency full path?

    ReplyDelete
  9. Is there any way to get this to work in the senario where your main project references a child project, and that child project has other dlls it references?

    Lets say you change your sample so that :

    AutoCopyIndierctDependenciesDemo.csproj (Remove Reference to)--> Npgsql.dll
    AutoCopyIndierctDependenciesDemo.csproj (References)--> Dependencies.csproj
    Dependencies.csproj (References)--> Npgsqldll
    Npgsql.dll (References) --> Mono.Security.dll

    When I build AutoCopyIndirectDependenciesDemo.csproj I would like the bin directory to contain:
    AutoCopyIndirectDependenciesDemo.exe
    Dependency.dll
    Npgsql.dll
    Mono.Security.dll

    ReplyDelete