Doraku / DefaultDocumentation

Create a simple markdown documentation from the Visual Studio xml one.
MIT No Attribution
157 stars 26 forks source link

Missing FileNameFactories #109

Closed TaffarelJr closed 2 years ago

TaffarelJr commented 2 years ago

I seem to have encountered a strange bug, where the FileNameFactories aren't loading properly.

I've imported the NuGet package into my project:

<ItemGroup>
  <PackageReference Include="DefaultDocumentation" Version="0.8.1" />
</ItemGroup>

I'm leaving all configuration set to default for now. But upon building the application, it throws (paths shortened):

..\DefaultDocumentation.targets(45,3): error MSB4018: The "DefaultDocumentationTask" task failed unexpectedly. [..\Utilities.csproj]
..\DefaultDocumentation.targets(45,3): error MSB4018: System.Exception: FileNameFactory 'FullName' not found [..\Utilities.csproj]
..\DefaultDocumentation.targets(45,3): error MSB4018:    at DefaultDocumentation.Internal.Context..ctor(JObject configuration, Type[] availableTypes) in D:\a\DefaultDocumentation\DefaultDocumentation\source\DefaultDocumentation.Common\Internal\Context.cs:line 20 [..\Utilities.csproj]
..\DefaultDocumentation.targets(45,3): error MSB4018:    at DefaultDocumentation.Internal.GeneralContext..ctor(JObject config, Type[] availableTypes, Settings settings, IReadOnlyDictionary`2 items) in D:\a\DefaultDocumentation\DefaultDocumentation\source\DefaultDocumentation.Common\Internal\GeneralContext.cs:line 30 [..\Utilities.csproj]
..\DefaultDocumentation.targets(45,3): error MSB4018:    at DefaultDocumentation.Generator..ctor(Target loggerTarget, IRawSettings settings) in D:\a\DefaultDocumentation\DefaultDocumentation\source\DefaultDocumentation.Common\Generator.cs:line 116 [..\Utilities.csproj]  
..\DefaultDocumentation.targets(45,3): error MSB4018:    at DefaultDocumentation.Generator.Execute(Target loggerTarget, IRawSettings settings) in D:\a\DefaultDocumentation\DefaultDocumentation\source\DefaultDocumentation.Common\Generator.cs:line 196 [..\Utilities.csproj]
..\DefaultDocumentation.targets(45,3): error MSB4018:    at DefaultDocumentation.DefaultDocumentationTask.Execute() in D:\a\DefaultDocumentation\DefaultDocumentation\source\DefaultDocumentation\DefaultDocumentationTask.cs:line 57 [..\Utilities.csproj]
..\DefaultDocumentation.targets(45,3): error MSB4018:    at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute() [..\Utilities.csproj]
..\DefaultDocumentation.targets(45,3): error MSB4018:    at Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask(ITaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask) [..\Utilities.csproj]

Poking around the source code, I can see in Generator.cs line 118 that it's supposed to be loading all the availabletypes:

new[] { typeof(Markdown.Writers.MarkdownWriter).Assembly.Location }
    .Concat(GetSetting<string[]>(nameof(settings.Plugins)) ?? Enumerable.Empty<string>())
    .Select(Assembly.LoadFrom)
    .SelectMany(a => a.GetTypes())
    .ToArray()

... and then in Context.cs line 19 it's sorting through them to find the specified FileNameFactory:

string fileNameFactory = GetSetting<string>(nameof(IRawSettings.FileNameFactory));
FileNameFactory = string.IsNullOrEmpty(fileNameFactory) ? null : availableTypes
    .Where(t => typeof(IFileNameFactory).IsAssignableFrom(t) && !t.IsAbstract)
    .Select(t => (IFileNameFactory)Activator.CreateInstance(t))
    .LastOrDefault(f => f.Name == fileNameFactory || $"{f.GetType().FullName} {f.GetType().Assembly.GetName().Name}" == fileNameFactory)
    ?? throw new Exception($"FileNameFactory '{fileNameFactory}' not found");

But I can't tell where the code would be failing to find the IFileNameFactory types. It all looks good to me. There must be some edge case I've stumbled on, where it's not working. My repo is a monorepo structure, so it's a bit different than a standard repo layout; and perhaps something in the way it's configured is the culprit.

I need to track this down, but I don't know how to go about debugging it further. I'd be happy to work with you, if you have any ideas.

petarpetrovt commented 2 years ago

I have the same problem.

Doraku commented 2 years ago

hum that's weird, you have no peculiar settings for DefaultDocumentation? what OS are you using?

petarpetrovt commented 2 years ago

I got multiple target frameworks. The error occurred on any OS in the github action. Doesn't happen in VS. build csproj

TaffarelJr commented 2 years ago

Mine is a monorepo - it contains several projects, each building a separate NuGet package.

I have a shared .props file where I can configure project settings that apply to all my NuGet packages. This is where I've installed DefaultDocumentation. I have no configuration for DD, other than this:

<Project>

  <PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <DefaultDocumentationFolder>$(SolutionDir)\wiki</DefaultDocumentationFolder>
    <DefaultDocumentationLogLevel>Trace</DefaultDocumentationLogLevel>
    <DisableDefaultDocumentation>false</DisableDefaultDocumentation>
    <IsPackable>true</IsPackable>
    ...
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="DefaultDocumentation" Version="0.8.1" />
    ...
  </ItemGroup>

</Project>
TaffarelJr commented 2 years ago

After adding at least one class to each NuGet package in my solution, I can confirm the folder was being overwritten by each project during the build process.

I've now moved the DefaultDocumentationFolder config setting out of the shared props, and each project is now pointing to its own independent output folder. This makes the docs generate successfully from VS.

The command line is still broken however. So it must be some difference in the way DD is loaded (or the app domain is configured) in VS vs from the dotnet CLI.

aremes commented 2 years ago

+1 on this one. I seem to have a similar problem to @TaffarelJr:

I investigated a bit and it seems like there may have been a break at v0.8.0.

I've attached a solution. dotnet build runs fine (and outputs md files) for 0.7.7 (which we used up to now), 0.7.8 and 0.7.9 but fails with 0.8.0. The error is the same: FileNameFactory 'FullName' not found.

I've attached an example. Just changing the reference to 0.7.9 in the csproj files makes it work with dotnet build. Will investigate further DefaultDocWorking.zip

aremes commented 2 years ago

With the exact same configuration, this: https://github.com/Doraku/DefaultDocumentation/blob/84afa18196236f3997808486cb4a9c1a8c8938cd/source/DefaultDocumentation.Common/Internal/Context.cs#L21 seems to return nothing with dotnet build whereas it contains all the factories when run in visual studio.. strange

aremes commented 2 years ago

Could this be related to https://github.com/dotnet/runtime/issues/44222 ?

I've tried all kinds of variations of <Private>false</Private>, <ExcludeAssets>runtime</ExcludeAssets>, <EnableDynamicLoading>true</EnableDynamicLoading> and <CopyLocalLocFileAssemblies>true</CopyLocalLockFileAssemblies> but i'm not getting anywhere.

Doraku commented 2 years ago

I have been using the dotnet cli locally and on github actions for a while without issue, I will have to check on my side if I can reproduce :/ (sorry I have not been available a lot lately I am in the process of moving home!)

I've now moved the DefaultDocumentationFolder config setting out of the shared props, and each project is now pointing to its own independent output folder.

You could have kept the shared configuration, just override the <DefaultDocumentationFolder> in each projects separately or use something like $(MSBuildProjectName) so that their output are different.

I've attached an example. Just changing the reference to 0.7.9 in the csproj files makes it work with dotnet build. Will investigate further

0.8.0 uses plugins to dynamically load available implementations for the file name factory while 0.7.9 is just an enum switch factory so they are not really comparable :/ If I can reproduce the problem I will add more log to see what dll is actually loaded and why types does not coincide correctly when run from the cli.

aremes commented 2 years ago

0.8.0 uses plugins to dynamically load available implementations for the file name factory while 0.7.9 is just an enum switch factory so they are not really comparable :/

Yeah, as i looked through the history in hopes of finding what changed the behavior i realized that the whole plug-in architecture actually started in 0.8.0 so that turned out to be a dead end.

I have been using the dotnet cli locally and on github actions for a while without issue, I will have to check on my side if I can reproduce :/

Well the fact that it works for you with the dotnet cli sounds promising. So it might just be an issue with tool/sdk versions, system configuration then?

In case it helps, i'm using VS2022 Enterprise (v17.2.0) and here's the --info output from dotnet:

.NET SDK (reflecting any global.json):
 Version:   6.0.300
 Commit:    8473146e7d

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.19043
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\6.0.300\

Host (useful for support):
  Version: 6.0.5
  Commit:  70ae3df4a6

.NET SDKs installed:
  6.0.300 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 3.1.25 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 3.1.25 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.1.25 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.5 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
aremes commented 2 years ago

I believe I have found the issue.

https://github.com/Doraku/DefaultDocumentation/blob/84afa18196236f3997808486cb4a9c1a8c8938cd/source/DefaultDocumentation.Common/Generator.cs#L118

This basically loads DefaultDocumentation.Markdown a second time alongside the one already loaded from the project reference. So now .IsAssignableFrom(typeof(IFileNameFactory)) fails because it thinks that the implemented IFileNameFactory is a different one (since it comes from a different assembly)

_context = new GeneralContext(
    _configuration,
    new[] { typeof(Markdown.Writers.MarkdownWriter).Assembly }
        .Concat(
            (GetSetting<string[]>(nameof(settings.Plugins)) ?? Enumerable.Empty<string>())
            .Select(Assembly.LoadFrom)
        )
        .SelectMany(a => a.GetTypes())
        .ToArray(),
    resolvedSettings,
    DocItemReader.GetItems(resolvedSettings));

should fix it.