LibraryManager icon indicating copy to clipboard operation
LibraryManager copied to clipboard

LibMan runs before dependent project targets

Open AlexanderRydberg opened this issue 3 months ago • 5 comments

Problem ProjectB generates files in a Target

<Target Name="RunLBeforeBuildStepgen" BeforeTargets="BeforeBuild" >
	<Message Text="=== Generating localizations ===" Importance="high" />
       <WriteLinesToFile File="Localizations.js" Lines="Localization data generated before build." />
</Target>

ProjectA have this in its libman.json

{
  "version": "1.0",
  "defaultProvider": "filesystem",
  "libraries": [
    {
      "library": "../../ProjectB/",
        "files": [ "Localizations.js" ],
      "destination": "wwwroot/assets/l10n/"
    }
  ]
}

dotnet build .\ProjectA\ProjectA\ProjectA.csproj fails:

Restore complete (0.3s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  ProjectA net8.0 failed with 2 error(s) (0.1s)
    D:\Nuget\microsoft.web.librarymanager.build\3.0.71\build\Microsoft.Web.LibraryManager.Build.targets(35,9): error
      System.AggregateException: One or more errors occurred. (The "../../ProjectB/" library could not be resolved by th
      e "filesystem" provider)
       ---> Microsoft.Web.LibraryManager.Contracts.InvalidLibraryException: The "../../ProjectB/" library could not be r
      esolved by the "filesystem" provider
         at Microsoft.Web.LibraryManager.Providers.FileSystem.FileSystemProvider.GetStreamAsync(ILibraryInstallationStat
      e state, String file, CancellationToken cancellationToken)
         --- End of inner exception stack trace ---
         at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
         at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
         at Microsoft.Web.LibraryManager.Providers.FileSystem.FileSystemProvider.<>c__DisplayClass9_1.<InstallAsync>b__0
      ()
         at Microsoft.Web.LibraryManager.Build.Contracts.HostInteraction.WriteFileAsync(String path, Func`1 content, ILi
      braryInstallationState state, CancellationToken cancellationToken)
         at Microsoft.Web.LibraryManager.Providers.FileSystem.FileSystemProvider.InstallAsync(ILibraryInstallationState
      desiredState, CancellationToken cancellationToken)
    libman.json : error LIB000: An unknown exception occurred

https://github.com/aspnet/LibraryManager/issues/642 describes a similar problem. But in my case its a target in a referenced project. And yes, also tried BeforeTargets="LibraryManagerRestore"

Created a little repo https://github.com/AlexanderRydberg/LibraryManagerTargetIssue

Expected behavior Targets in referenced project being run before libman.

Is this behavior by design? Any suggestions on how I could solve this without: manually building ProjectB first and then ProjectA or Copy Target from ProjectB to ProjectA's csproj and have BeforeTargets="LibraryManagerRestore"

AlexanderRydberg avatar Oct 27 '25 10:10 AlexanderRydberg

@jimmylewis do you have any suggestions on how to work around this 🙏 ? Or should I clearify anything in the issue?

AlexanderRydberg avatar Nov 26 '25 06:11 AlexanderRydberg

I'm still investigating. From my understanding, MSBuild should recognize that ProjectA having a ProjectReference to ProjectB requires B to build first. But in the clean case, it's clearly running the LibMan target in ProjectA before ProjectB has built. I need to see if there's a better way to schedule the target after ProjectA's dependencies have built.

However, when I build the slnx in Visual Studio, it correctly identifies the build order and I can't reproduce the problem. So this might be a bug in dotnet/msbuild CLI. The build order is also correct in the Project Build Dependencies wizard (right-click menu on the solution) and can't be changed.

As a side note, I did notice that the error message in 3.0.71 is bad, but I think this was fixed in #794. Error message from 3.0.71:

      System.AggregateException: One or more errors occurred. (The "../../ProjectB/" library could not be resolved by 
      the "filesystem" provider)

Note that this is "library not resolved" which is weird. There's also a LIB000 unknown failure.

Error message from tip (3.0.106):

ProjectA net8.0 failed with 1 error(s) (0.0s)
    libman.json : error LIB018:
      "../../ProjectB/" does not contain the following: Localizations.js
      Valid files are Program.cs, ProjectB.csproj

jimmylewis avatar Nov 29 '25 02:11 jimmylewis

Of course, I had an inspiration right after posting that. If I use a SLN file instead of SLNX, it also works correctly, building B before A even from clean. So this is a bug in SLNX build on the CLI.

jimmylewis avatar Nov 29 '25 02:11 jimmylewis

I played around with the slnx a little more. Explicitly setting the build dependency seems to be a workaround:

<Solution>
  <Project Path="../ProjectB/ProjectB.csproj" />
  <Project Path="ProjectA/ProjectA.csproj">
    <BuildDependency Project="../ProjectB/ProjectB.csproj" />
  </Project>
</Solution>

jimmylewis avatar Nov 29 '25 03:11 jimmylewis

First of all, I really appreciate you taking the time to check on this. Isnt this an interesting issue 😄 !?

Yeah you are correct that everything works well in Visual Studio. Imagine my frustration when it didnt work on our buildserver 😅

Im testing a bit more now. No changes on the repo.

Msbuild version

MSBuild version 18.0.5+e22287bf1 for .NET Framework
18.0.5.56406

Building / Rebuilding slnx with msbuild works:

$msbuildPath = "C:\Program Files\Microsoft Visual Studio\18\Professional\MSBuild\Current\Bin\MSBuild.exe"

# Make sure we always start from a clean state
&"$msbuildPath" .\ProjectA\ProjectA.slnx /r '/t:Clean' /m /p:Configuration=Release
&"$msbuildPath" .\ProjectA\ProjectA.slnx /r '/t:Restore;Rebuild' /m /p:Configuration=Release
&"$msbuildPath" .\ProjectA\ProjectA.slnx /r '/t:Clean' /m /p:Configuration=Release
&"$msbuildPath" .\ProjectA\ProjectA.slnx /r '/t:Build' /m /p:Configuration=Release
$msbuildPath = "C:\Program Files\Microsoft Visual Studio\18\Professional\MSBuild\Current\Bin\MSBuild.exe"

&"$msbuildPath" .\ProjectA\ProjectA\ProjectA.csproj '/t:Clean' /m /p:Configuration=Release
&"$msbuildPath" .\ProjectB\ProjectB.csproj /r '/t:Restore;Build' /m /p:Configuration=Release
cat ProjectB\Localizations.js
pause
&"$msbuildPath" .\ProjectA\ProjectA\ProjectA.csproj /r '/t:Restore;Build' /m /p:Configuration=Release
cat ProjectB\Localizations.js

ProjectB\Localizations.js contains two rows:

Localization data generated before build.
Localization data generated before build.

We here know that ProjectA is triggering a build of ProjectB but its too slow. This is also what we see if we build the slnx with dotnet build.

dotnet build .\ProjectA\ProjectA.slnx
Restore complete (0.4s)
  ProjectA net8.0 failed with 2 error(s) (0.0s)
    D:\Nuget\microsoft.web.librarymanager.build\3.0.71\build\Microsoft.Web.LibraryManager.Build.targets(35,9): error
      System.AggregateException: One or more errors occurred. (The "../../ProjectB/" library could not be resolved by the "filesystem" provider)
       ---> Microsoft.Web.LibraryManager.Contracts.InvalidLibraryException: The "../../ProjectB/" library could not be resolved by the "filesystem" provider
         at Microsoft.Web.LibraryManager.Providers.FileSystem.FileSystemProvider.GetStreamAsync(ILibraryInstallationState state, String file, CancellationToken cancellationToken)
         --- End of inner exception stack trace ---
         at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
         at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
         at Microsoft.Web.LibraryManager.Providers.FileSystem.FileSystemProvider.<>c__DisplayClass9_1.<InstallAsync>b__0()
         at Microsoft.Web.LibraryManager.Build.Contracts.HostInteraction.WriteFileAsync(String path, Func`1 content, ILibraryInstallationState state, CancellationToken cancellationToken)
         at Microsoft.Web.LibraryManager.Providers.FileSystem.FileSystemProvider.InstallAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken)
    libman.json : error LIB000: An unknown exception occurred

  ProjectB net8.0 succeeded (0.3s) → ProjectB\bin\Debug\net8.0\ProjectB.dll

The second build will now succeed.

Can confirm that the issue doesnt exist on sln. This works as expected:

dotnet new sln -n Project
dotnet sln Project.sln add .\ProjectA\ProjectA\ProjectA.csproj
dotnet sln Project.sln add .\ProjectB\ProjectB.csproj

dotnet build .\Project.sln

And also works with msbuild:

$msbuildPath = "C:\Program Files\Microsoft Visual Studio\18\Professional\MSBuild\Current\Bin\MSBuild.exe"

&"$msbuildPath" .\Project.sln '/t:Clean' /m /p:Configuration=Release
&"$msbuildPath" .\Project.sln /r '/t:Restore;Rebuild' /m /p:Configuration=Release
&"$msbuildPath" .\Project.sln '/t:Clean' /m /p:Configuration=Release
&"$msbuildPath" .\Project.sln /r '/t:Build' /m /p:Configuration=Release

Maybe this is a bug within msbuild like you suggested. I will open a issue on their repo as well.

AlexanderRydberg avatar Nov 29 '25 15:11 AlexanderRydberg