phmonte / Buildalyzer

A utility to perform design-time builds of .NET projects without having to think too hard about it.
MIT License
589 stars 92 forks source link

`PublishSingleFile` breaks `IProjectAnalyzer.Build()` #224

Closed chucker closed 2 weeks ago

chucker commented 1 year ago

If I publish a .NET 6 app with PublishSingleFile enabled (PublishReadyToRun and PublishTrimmed don't seem to be related to the issue), it hangs as soon as I call Build().

For example, make a fresh class library (its code doesn't matter), and a console app with the following Program.cs:

using Buildalyzer;

var manager = new AnalyzerManager();
var analyzer = manager.GetProject(@"path\to\ClassLibrary1\ClassLibrary1.csproj");

Console.WriteLine("1");

_ = analyzer.Build();

Console.WriteLine("2");

If you build and run it, you get both console outputs within a few seconds.

But if you then create a publish profile with PublishSingleFile enabled, e.g.:

<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
  <PropertyGroup>
    <Configuration>Release</Configuration>
    <Platform>Any CPU</Platform>
    <PublishDir>publish\</PublishDir>
    <PublishProtocol>FileSystem</PublishProtocol>
    <_TargetId>Folder</_TargetId>
    <TargetFramework>net6.0</TargetFramework>
    <RuntimeIdentifier>win-x86</RuntimeIdentifier>
    <SelfContained>true</SelfContained>
    <PublishSingleFile>true</PublishSingleFile>
  </PropertyGroup>
</Project>

and run that, it seems to hang after the first output.

My first hunch was that code trimming removed something crucial, but that seems to be inactive.

I tried to profile it, and it looks like some kind of deadlock?

image
phmonte commented 4 months ago

Hi @chucker , I'll look into it and get back to you.

Karql commented 2 months ago

Hi πŸ˜‰ Any update on this topic? We also faced this issue.

Corniel commented 2 months ago

If @phmonte does not have time today (nor tomorrow), I'll give it a go.

phmonte commented 2 months ago

Hello @Karql and @chucker

I tried to simulate the problem, but all attempts worked without errors.

I created a unit test to simulate the problem and a project in my personal git, could you assess whether the configuration is correct?

What version of Buildalyzer are you using?

Karql commented 2 months ago

@phmonte

The problem occurs when you publish code that uses Buildalyzer in PublishSingleFile=true mode. Not when you analyze a project with a such profile πŸ˜‰

I don't know how to simulate it in unit tests but your sample project is good for replicate this behavior.

I have change one line

from: var analyzer = manager.GetProject(@"../../../../ClassLibrary1/ClassLibrary1.csproj"); to: var analyzer = manager.GetProject(@"../ClassLibrary1/ClassLibrary1.csproj");

and start console in ConsoleApp1 folder.

image

As you can see for normal build everything works as expected

but when published as single file: dotnet publish -p PublishSingleFile=true ConsoleApp1.csproj

and runs bin/Debug/net6.0/win-x64/publish/ConsoleApp1.exe its hangs (i have cancelled it after half minute).

It hangs somewhere in AnonymousPipeLoggerServer starting from this line: https://github.com/phmonte/Buildalyzer/blob/main/src/Buildalyzer/ProjectAnalyzer.cs#L182

Best regards, Mateusz

phmonte commented 2 months ago

@Karql thanks, now it's possible to reproduce, I'm investigating.

phmonte commented 2 months ago

I believe I found the problem, it's here. It uses the dll address to send to msbuild, I'm testing some alternatives.

Karql commented 2 months ago

Nice catch πŸ‘Œ

https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?tabs=cli#api-incompatibility

This could be a problem... As I understand correctly, that dll needs to exists on the disk in order to pass its path to msbuild.

For a moment, I wondered if it might be possible to get extract path from DOTNET_BUNDLE_EXTRACT_BASE_DIR https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables#dotnet_bundle_extract_base_dir

but from .NET5.0 only native dlls are extracted: https://github.com/dotnet/runtime/issues/43010

I hope you will find some alternatives πŸ˜‰

Two ideas: 1) Exclude logger dll from single file. 2) Add possibility to specify path to logger similar like MSBUILD_EXE_PATH

phmonte commented 2 months ago

The msbuild result is captured through the log, there is a custom log that does all this work, removing it will be a bit complex, I'm thinking about other alternatives keeping the dll.

phmonte commented 2 months ago

Hi @Karql , I did some tests in the last few days, from extracting a dll to manipulating the assembly, unfortunately I was not successful. I believe the healthiest solution would be to remove the Buildalyzer.Logger package from the publish single file. There are some alternatives in this issue.

If you are really going to remove the dll from the publish single file and you are unsuccessful with the alternatives, let us know and we can think of an alternative (search in the current directory or by parameter).

Karql commented 2 months ago

@phmonte

I have seen the issue you mentioned πŸ˜‰ Did you manage to use this solution?

For me after use it its still hangs.

ConsoleApp1.Program.cs:

using Buildalyzer;
using Buildalyzer.Logger;

string loggerPath = typeof(BuildalyzerLogger).Assembly.Location;
Console.WriteLine($"Logger path: {loggerPath}");

var manager = new AnalyzerManager();
var analyzer = manager.GetProject(@"../ClassLibrary1/ClassLibrary1.csproj");

Console.WriteLine("1");

_ = analyzer.Build();

Console.WriteLine("2");

ConsoleApp1.csproj

...
  <Target Name="ExplicitRemoveFromFilesToBundle" BeforeTargets="GenerateSingleFileBundle" DependsOnTargets="PrepareForBundle">
    <ItemGroup>
      <FilesToRemoveFromBundle Include="@(FilesToBundle)" Condition="$([System.String]::new('%(Filename)').ToLower().Contains('buildalyzer.logger'))" />
    </ItemGroup>
    <Message Text="FilesToRemoveFromBundle '@(FilesToRemoveFromBundle)'" Importance="high" />
    <ItemGroup>
      <FilesToBundle Remove="@(FilesToRemoveFromBundle)" />
    </ItemGroup>
  </Target>

  <Target Name="CopyFilesToRemoveFromBundle" AfterTargets="Publish">
    <Copy SourceFiles="@(FilesToRemoveFromBundle)" DestinationFolder="$(PublishDir)" />
    <Message Text="Copied files to remove from bundle to '$(PublishDir)'" Importance="high" />
  </Target>
...

image

but there is a small improvement - the path to the logger is good πŸ˜‰

Screen from dump looks similar: image

phmonte commented 2 months ago

@Karql I ended up forgetting to mention it.

There are 2 dlls, could you test by removing them? -MsBuildPipeLogger.Logger.dll -Buildalyzer.logger.dll

I really believe it will solve your problem.

Karql commented 2 months ago

@phmonte

I can confirm that after excluding those two dlls application works as expected πŸ˜‰

I wonder if adding some fallbacks would be a nice addition (somthing like searching msbuild here: https://github.com/dotnet/msbuild/blob/main/src/Shared/BuildEnvironmentHelper.cs#L77).

I have three in my mind:

phmonte commented 2 months ago

Thanks for confirming, I believe it's not the best solution, but it's the only one that enables PublishSingleFile due to current restrictions.

I will make some of your suggestions.

Karql commented 2 months ago

btw. I really appreciate your help πŸ˜‰

Have a great day!

phmonte commented 2 weeks ago

@Karql an environment variable was added for publish single file cases in version 7.0.2. Environment variable name: LoggerPathDll

I'll close the issue, any problems open a new one.

Karql commented 2 weeks ago

@phmonte πŸ‘Œ