Msbuild: Allow copying empty directories with <Content>

Created on 19 Jan 2017  路  14Comments  路  Source: dotnet/msbuild

This issue has been inspired by dotnet/cli#2911. In short, if I specify something like this:

<ItemGroup>
    <Content Include="logs">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
</ItemGroup>

MSBuild will throw an error saying that the Copy task does not support copying directories. I can put in a glob, but if there are no files, nothing gets copied out (with a glob the why of that behavior is clear to me at least).

/cc @moozzyk

Most helpful comment

Ok, so publishing in VS is sorted now, but I'm guessing the FileSystemPublish target doesn't run when using the cli dotnet publish src\myproj.csproj --framework net462 --output "c:\temp" --configuration Release since the log folder is not present at c:\temp\logs.

So I've now got one target for cli/VSCode, and one for VS:

  <Target Name="CreateLogsFolderDuringCliPublish" AfterTargets="AfterPublish">
    <MakeDir Directories="$(PublishDir)logs" Condition="!Exists('$(PublishDir)logs')" />
  </Target>

  <Target Name="CreateLogsFolderDuringVSPublish" AfterTargets="FileSystemPublish">
    <MakeDir Directories="$(PublishUrl)logs" Condition="!Exists('$(PublishUrl)logs')" />
  </Target>

I can go home now :)

All 14 comments

The referenced issue should address the fact that the ANCM cannot currently create the path specified in web.config for stdout logs. https://github.com/aspnet/AspNetCoreModule/issues/43

The suggestion here is still great imo: I'd like to be able to create empty folders without having to resort to ...

<Target Name="CreateLogsFolder" AfterTargets="AfterPublish">
  <MakeDir Directories="$(PublishDir)logs" Condition="!Exists('$(PublishDir)logs')" />
</Target>

We're struggling with this also, after the JSON to csproj migration. GuardRex, how does your CreateLogsFolder target work? I've tried exactly that, and the logs folder isn't created in the final default publish location of .\bin\Debug\PublishOutput. I suspect $PublishDir is that temporary publish location, before the final move?

@WildBamboo That :point_up: does work here. Are you using the Microsoft.NET.Sdk.Web SDK? Here's a test proj sample csproj showing how I have it set up:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
    <RuntimeFrameworkVersion>1.1.1</RuntimeFrameworkVersion>
    <WarningsAsErrors>true</WarningsAsErrors>
    <OutputType>Exe</OutputType>
    <Optimize Condition=" '$(Configuration)' != 'Debug' ">true</Optimize>
    <MvcRazorCompileOnPublish Condition=" '$(Configuration)' == 'Release' ">true</MvcRazorCompileOnPublish>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="1.1.1" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="1.1.0" PrivateAssets="All" />
  </ItemGroup>

  <Target Name="CreateLogsFolder" AfterTargets="AfterPublish">
    <MakeDir Directories="$(PublishDir)Logs" Condition="!Exists('$(PublishDir)Logs')" />
  </Target>

</Project>

capture

If directories could be used as items, it would also be great if it were possible to use their timestamps for incremental compilation (=> usable as Inputs and Outputs for targets).

@GuardRex I've just made a new ASP.NET Core Web Application (.NET Core) project, using the ASP.NET Core 1.1 empty template, and added your target. I published to the filesystem, default location, and no logs folder is present.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" />
    <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
  </ItemGroup>

  <Target Name="CreateLogsFolder" AfterTargets="AfterPublish">
    <MakeDir Directories="$(PublishDir)Logs" Condition="!Exists('$(PublishDir)Logs')" />
  </Target>

</Project>

So I dunno; works on your machine :)

@dasMulli Can you think of a reason why it might not work? Does it need an entry point to work?

@WildBamboo btw - the wwwroot folder is handled by the Sdk.Web, so you can drop that <ItemGroup> if you want. It shouldn't hurt anything tho if you leave it there.

@GuardRex, wwwroot tip noted; that was just how the template came.

Once you think you understand how things work, VS comes along and does things differently :trollface:

It really publishes the app to obj\Release\netcoreapp1.1\PubTmp\Out and then copies things around - but not the Logs folder. Doesn't appear to be target ordering either because even if I use BeforeTargets="PerpareForPublish", the web publish targets pretend it didn't happen.

If I understand it correctly, the logic for it lives somewhere in https://github.com/aspnet/websdk/tree/dev/src/Publish, maybe we can move this issue over there?

@dasMulli So the reason it works for me is that I'm using VS Code! :trollface: lol ... which runs right on the CLI without so much VS magic.

@WildBamboo Yes, open an issue over on websdk ... but actually, I think there already is an issue over there about the Logs folder (I think). Search and see if you can piggyback on that issue (IIRC about it being there).

However, I'm guessing the web targets would also need support for folders as items to get that out of the box - so we're back here.

In the meantime, we can of course hook into file system publish:

<Target Name="CreateLogsFolderOnFileSystem" AfterTargets="FileSystemPublish">
  <MakeDir Directories="$(PublishUrl)Logs" Condition="!Exists('$(PublishUrl)Logs')" />
</Target>

Thanks all. I can't see any issue related to the logs folder, so I created https://github.com/aspnet/websdk/issues/152

Currently using the workaround suggested by @dasMulli above, which works great for what we need right now.

Ok, so publishing in VS is sorted now, but I'm guessing the FileSystemPublish target doesn't run when using the cli dotnet publish src\myproj.csproj --framework net462 --output "c:\temp" --configuration Release since the log folder is not present at c:\temp\logs.

So I've now got one target for cli/VSCode, and one for VS:

  <Target Name="CreateLogsFolderDuringCliPublish" AfterTargets="AfterPublish">
    <MakeDir Directories="$(PublishDir)logs" Condition="!Exists('$(PublishDir)logs')" />
  </Target>

  <Target Name="CreateLogsFolderDuringVSPublish" AfterTargets="FileSystemPublish">
    <MakeDir Directories="$(PublishUrl)logs" Condition="!Exists('$(PublishUrl)logs')" />
  </Target>

I can go home now :)

I have another problem with it - I cannot have empty AppData folder on publish.
In my specific case I want to test sqlite database to be initialized and filled with test data, but before I have to open it and I get "Unable to open file" error, which I dont get even when folder is empty and there is no file.
I believe there is more cases like that where something cannot check something when folder from path don't exist

Slightly off target but I thought it worth mentioning this here as I got to this thread trying to resolve my problem 'How can I create a folder when I publish my MVC 4 app in VS Code'.

The key to solving what you need to do is to look at the output of the publish process. I have a 'publish' process in my '.vscode/tasks.json' file thus:

        {
            "label": "publish",
            "type": "shell",
            "command": "msbuild",
            "args": [
                // Ask msbuild to generate full paths for file names.
                "/property:GenerateFullPaths=true",
                "/t:build",
                "/p:Platform=AnyCPU",
                "/p:Configuration=Release",
                "/p:DeployOnBuild=true", 
                "/p:PublishProfile=FolderProfile",
                // Do not generate summary otherwise it leads to duplicate errors in Problems panel
                "/consoleloggerparameters:NoSummary"
            ],
            "group": "build",
            "presentation": {
                // Reveal the output only if unrecognized errors occur.
                "reveal": "silent"
            },
            // Use the standard MS compiler pattern to detect errors, warnings and infos
            "problemMatcher": "$msCompile"
        }

When this runs, open the terminal window (click on the 'Building..' in the taskbar), and check the headings in the output window printed in blue. When I was building an old MVC 4 app I got headings like this:
AfterWebPublish:
PreAutoParameterizationWebConfigConnectionStrings:

To trigger my post publish task to create an output folder, I created an section like this in my .csproj file:

  <Target Name="AfterWebPublish" AfterTargets="WebFileSystemPublish">
    <MakeDir Directories="$(PublishUrl)\logs" Condition="!Exists('$(PublishUrl)\logs')" />
  </Target>

N.B. The name of the event in AfterTargets="XYZ" needs to match the name of the event observed earlier.

Was this page helpful?
0 / 5 - 0 ratings