3

I have a project with some nuget packages referenced.

In output folders (bin\Debug or bin\Release), all referenced libraries lie next to the executable.

How to specify output folder for libraries?
I want all nuget libraries in bin\Release\Libs and executable in bin\Release.

baruchiro
  • 5,088
  • 5
  • 44
  • 66
viter.alex
  • 99
  • 1
  • 6
  • You can use OutputPath attribute while installing nuget package to install in particular folder – Apoorva Raju May 02 '19 at 05:28
  • Hi alex, What's your Project type? Please check https://docs.microsoft.com/en-us/nuget/consume-packages/managing-the-global-packages-and-cache-folders#viewing-folder-locations and packages are installed to the global-packages folder, then copied into the project's packages folder, build the project and I can only found the projectname.dll and projectname.exe under bin\Debug or bin\Release, do you want change those files location? If so, please right click the project name and unload, edit the project file like xxx.csproj and modify value of 'OutputPath' for Debug\Release, reload the solution. – Sara Liu - MSFT May 02 '19 at 08:41
  • If you want to change the installation location of NuGet packages, we can create a project level config file 'nuget,config' under the solution folder, use to configure it, please check this similar issue: https://stackoverflow.com/questions/4092759/is-it-possible-to-change-the-location-of-packages-for-nuget – Sara Liu - MSFT May 02 '19 at 08:44
  • @SaraLiu-MSFT, I have simple console application project. Add NuGet package, for example Newtonsoft.Json. After build I get all files in output folder `bin\Release`. When I want to get libraries in `bin\Release\Lib` – viter.alex May 02 '19 at 15:02
  • 1
    @viter.alex I'm not aware of anything built-in to change the location that assemblies are copied to on build/publish. Are you aware that .NET won't by default load assemblies in sub directories, because you'll need to configure your app to load from that path in addition to the build file changes. – zivkan May 03 '19 at 00:50
  • 1
    Anyway, if you're determined to do it, I think you'll need to become somewhat of a build expert. The [MSBuild Structured Log Viewer](http://msbuildlog.com/) is a great tool that will help you in this. You need to see how the SDK copies NuGet dlls to the output directory, see if there are any item properties you can modify to do what you want and if so, write a MSBuild target that runs and updates the relevant items to add/change the properties needed. – zivkan May 03 '19 at 00:53
  • @zivkan, thank you for this great tool. I'll try to use it to solve my problem. – viter.alex May 03 '19 at 05:01

2 Answers2

6

I woke up early this morning and decided to have a go at doing it myself. Turned out to be pretty quick, but that may be because of my (unfortunate) experience with looking into MSBuild files. Writing this post took me far longer than writing the target.

From your question, I assume you're using a traditional project, since SDK style projects only create the project's assembly in the bin directory. However, I much prefer SDK style projects because use can quickly and easily use the dotnet cli to create test projects and the csproj is much more easily editable. So, I'll give you my steps to find my solution for SDK style projects, and you need to follow along to do something similar with a traditional project.

So, we want to change where a files are being copied, which means we need to modify some items. Everything in MSBuild runs in a target, so we'll need to know when to run our custom target, what items to modify and probably what metadata of those items to modify. I created a new project, added some NuGet references then ran dotnet msbuild -t:publish -bl and opened the msbuild.binlog file.

What metadata to change

Searching for the name of a dll that came from a nuget package, I find a message saying copied from ... to ..., so I click on it to go to the entry, and follow the tree back to the task, which I see is the built-in Copy task. The target path to the task is Publish -> _PublishBuildAlternative -> ComputeAndCopyFilesToPublisDirectory -> CopyFilesToPublishDIrectory -> _CopyResolvedFilesToPublishAlways. Double clicking the copy task I see

    <Copy SourceFiles = "@(_ResolvedFileToPublishAlways)"
          DestinationFiles="@(_ResolvedFileToPublishAlways->'$(PublishDir)%(RelativePath)')"
          OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
          Retries="$(CopyRetryCount)"
          RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
          UseHardlinksIfPossible="$(CreateHardLinksForPublishFilesIfPossible)"
          UseSymboliclinksIfPossible="$(CreateSymbolicLinksForPublishFilesIfPossible)">

So, I can guess I need to modify the RelativePath metadata of an _ResolvedFileToPublishAlways item.

What item to change

Side note: MSBuild doesn't have public/private modifies, so instead a convention is generally used. Anything starting with an underscore should be considered to be an implementation detail that could change between releases, so it's better to use things that do not start with an underscore, and the teams who maintain the targets file should try harder not to break compatibility.

So, since _ResolvedFileToPublishAlways starts with an underscore, let's find out where it was created. Searching for it takes me to a target where the binlog tells me it was added, in a target called _ComputeResolvedFilesToPublishTypes, and its definition is

  <Target Name="_ComputeResolvedFilesToPublishTypes">
    <ItemGroup>
      <_ResolvedFileToPublishPreserveNewest Include="@(ResolvedFileToPublish)"
                                             Condition="'%(ResolvedFileToPublish.CopyToPublishDirectory)'=='PreserveNewest'" />

      <_ResolvedFileToPublishAlways Include="@(ResolvedFileToPublish)"
                                     Condition="'%(ResolvedFileToPublish.CopyToPublishDirectory)'=='Always'" />
    </ItemGroup>
  </Target>

So, I can see that it's simply copying ResolvedFileToPublish items to new item names. Looking for where those items are created, it's in a target named ComputeFilesToPublish, and expanding the tree to see all the items created and their metadata, I'm going to guess the items I want to modify all have AssetType = runtime, which is perfect for a condition we're going to need to use.

When to run our target

Ideally I would run just before CopyFilesToPublishDirectory, however looking at its definition I see

  <Target Name="CopyFilesToPublishDirectory"
          DependsOnTargets="_CopyResolvedFilesToPublishPreserveNewest;
                            _CopyResolvedFilesToPublishAlways" />

The problem is that when MSBuild executes a target it runs in this order:

  1. Any targets listed in DependsOnTargets
  2. Any target that lists the current target as BeforeTargets
  3. The current target
  4. Any targets that lists the current target as AfterTargets

So, while I want to run BeforeTargets='CopyFilesToPublishDirectory', the DependsOnTargets will run before my target, so I can't do that. So I'll choose to run AfterTargets="ComputeFilesToPublish". There are other targets that run in between those, and one sounds like that it might add ResolvedFileToPublish items, but with my current project the target doesn't run because of conditions, so my custom target might not be generic enough to work for all projects.

Writing our custom target

So now we know when our target will run, which items it will modify and how we will modify their metadata.

  <Target Name="RedirectRuntimeFilesToBinDirectory" AfterTargets="ComputeFilesToPublish">
    <ItemGroup>
      <ResolvedFileToPublish Condition=" '%(ResolvedFileToPublish.AssetType)' == 'runtime' ">
        <RelativePath>lib\%(RelativePath)</RelativePath>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>

Unfortunately the binlog doesn't show the details about the metadata being modified, which is a real pain in the arse when trying to debug build issues and why some items have unexpected values, but in any case I've now successfully changed the destination of NuGet dependencies, and probably project to project references, to a lib\ directory.

zivkan
  • 12,793
  • 2
  • 34
  • 51
4

Grace to the zivkan's investigation I found the answer. Traditional project has target CopyFilesToOutputDirectory which depends on _CopyFilesMarkedCopyLocal target. In this last one we have task Copy:

<Copy
    SourceFiles="@(ReferenceCopyLocalPaths)"
    DestinationFiles="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')"
    SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
    OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
    Retries="$(CopyRetryCount)"
    RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
    UseHardlinksIfPossible="$(CreateHardLinksForCopyLocalIfPossible)"
    UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyLocalIfPossible)"
    Condition="'$(UseCommonOutputDirectory)' != 'true'"
        >

And here I found metadata DestinationSubDirectory which is exactly what I need to change.

So finally

First, we need to change csproj file and add these lines:

  <ItemDefinitionGroup>
    <ReferenceCopyLocalPaths>
      <DestinationSubDirectory>lib\</DestinationSubDirectory>
    </ReferenceCopyLocalPaths>
  </ItemDefinitionGroup>

Second, we need to change app.config file to let the assembly know the path to the libraries:

  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="lib;libs" />
    </assemblyBinding>
  </runtime>

That's all. All referenced libraries will be copied into subfolder lib

viter.alex
  • 99
  • 1
  • 6
  • 1
    Hi alex, we are so glad to hear that your issue is solved, please mark your reply as answer when you have free time and that will help other community members to easier search it, thanks in advance :) – Sara Liu - MSFT May 06 '19 at 06:53