dotnet / dotnet/project-system
Inconsistent copy of transitive assembly references with FastUpToDate build
- Dominant language
- C#
- Stars
- 1k
- Forks
- 415
- PR merge metrics
- No merged PRs in 30d
Description
## Visual Studio Version
17.9.2
## Summary
With the build acceleration option, there are now two build behaviours in Visual Studio - normal MSBuild build and the FastUpToDate build. We have found a case where the FastUpToDate build would copy additional output files where a normal build would not. This happens when a referenced project has a further assembly reference. This extra assembly would not normally be copied to the output of the top level project, but under FastUpToDate there is an extra copy done.
## Steps to Reproduce
1. Create a new C# Console App called `ConsoleApp1` in a solution named `TestFastUpToDate` (with the solution and project not in the same directory), targeting .NET 8.0.
2. To the solution, add a new Class Library project called ClassLibrary1, targeting .NET 8.0.
3. To the solution, add another new Class Library called ClassLibrary2, targeting .NET 8.0.
4. Rebuild ClassLibrary2 (to ensure the output of ClassLibrary2 is created).
5. In ClassLibrary1.csproj, add an assembly reference to the output of ClassLibrary2. This adds a `` item with its HintPath.
```
..\ClassLibrary2\bin\Debug\net8.0\ClassLibrary2.dll
```
6. In ConsoleApp1, add a project reference to ClassLibrary1.
```
```
8. Note that we now have these three project files.
**ClassLibrary1.csproj**
```
net8.0
enable
enable
..\ClassLibrary2\bin\Debug\net8.0\ClassLibrary2.dll
```
**ClassLibrary2.csproj**
```
net8.0
enable
enable
```
**ConsoleApp1.csproj**
```
Exe
net8.0
enable
enable
```
9. We will now monitor the output directory of ConsoleApp1 (`C:\Temp\TestFastUpToDate\ConsoleApp1\bin\Debug\net8.0` for my project) to see what files are placed there from the build.
## Expected Behavior
**Building or rebuilding the solution with the full build will behave the same way as a 'FastUpToDate' build.**
## Actual Behavior
**A 'FastUpToDate' build copies an additional file to the output directory, which a rebuild or normal build does not copy.**
This is the output from a normal build (Build -> Build Solution) when the output directory of ConsoleApp1 is empty:
```
Build started at 14:50...
1>FastUpToDate: Build acceleration is enabled for this project via a feature flag. See "Tools | Options | Environment | Preview Features" to control this setting. See https://aka.ms/vs-build-acceleration. (ClassLibrary2)
1>FastUpToDate: Comparing timestamps of inputs and outputs: (ClassLibrary2)
1>FastUpToDate: No inputs are newer than earliest output 'C:\Temp\TestFastUpToDate\ClassLibrary2\obj\Debug\net8.0\ClassLibrary2.pdb' (2024-03-12 14:50:08.069). Newest input is 'C:\Temp\TestFastUpToDate\ClassLibrary2\obj\ClassLibrary2.csproj.nuget.g.targets' (2024-03-12 14:48:10.754). (ClassLibrary2)
1>FastUpToDate: Project is up-to-date. (ClassLibrary2)
2>FastUpToDate: Build acceleration is enabled for this project via a feature flag. See "Tools | Options | Environment | Preview Features" to control this setting. See https://aka.ms/vs-build-acceleration. (ClassLibrary1)
2>FastUpToDate: Comparing timestamps of inputs and outputs: (ClassLibrary1)
2>FastUpToDate: No inputs are newer than earliest output 'C:\Temp\TestFastUpToDate\ClassLibrary1\obj\Debug\net8.0\ClassLibrary1.pdb' (2024-03-12 14:50:20.040). Newest input is 'C:\Temp\TestFastUpToDate\ClassLibrary2\bin\Debug\net8.0\ClassLibrary2.dll' (2024-03-12 14:50:08.070). (ClassLibrary1)
2>FastUpToDate: Project is up-to-date. (ClassLibrary1)
3>FastUpToDate: Build acceleration is enabled for this project via a feature flag. See "Tools | Options | Environment | Preview Features" to control this setting. See https://aka.ms/vs-build-acceleration. (ConsoleApp1)
3>FastUpToDate: Comparing timestamps of inputs and outputs: (ConsoleApp1)
3>FastUpToDate: Output 'C:\Temp\TestFastUpToDate\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.dll' does not exist, not up-to-date. (ConsoleApp1)
3>------ Build started: Project: ConsoleApp1, Configuration: Debug Any CPU ------
3>Build started 2024/03/12 14:50:52.
3>Target _GetProjectReferenceTargetFrameworkProperties:
3>Target ResolveProjectReferences:
3>Target GenerateTargetFrameworkMonikerAttribute:
3> Skipping target "GenerateTargetFrameworkMonikerAttribute" because all output files are up-to-date with respect to the input files.
3>Target CoreGenerateAssemblyInfo:
3> Skipping target "CoreGenerateAssemblyInfo" because all output files are up-to-date with respect to the input files.
3>Target _GenerateSourceLinkFile:
3> Source Link is empty, file 'obj\Debug\net8.0\ConsoleApp1.sourcelink.json' does not exist.
3>Target CoreCompile:
3> Skipping target "CoreCompile" because all output files are up-to-date with respect to the input files.
3>Target _CreateAppHost:
3> Skipping target "_CreateAppHost" because all output files are up-to-date with respect to the input files.
3>Target _CopyFilesMarkedCopyLocal:
3> Copying file from "C:\Temp\TestFastUpToDate\ClassLibrary1\bin\Debug\net8.0\ClassLibrary1.pdb" to "C:\Temp\TestFastUpToDate\ConsoleApp1\bin\Debug\net8.0\ClassLibrary1.pdb".
3> Copying file from "C:\Temp\TestFastUpToDate\ClassLibrary1\bin\Debug\net8.0\ClassLibrary1.dll" to "C:\Temp\TestFastUpToDate\ConsoleApp1\bin\Debug\net8.0\ClassLibrary1.dll".
3> Touching "C:\Temp\TestFastUpToDate\ConsoleApp1\obj\Debug\net8.0\ConsoleA.CB28C8CC.Up2Date".
3>Target GetCopyToOutputDirectoryItems:
3> Target _GetCopyToOutputDirectoryItemsFromTransitiveProjectReferences:
3>Target _CopyOutOfDateSourceItemsToOutputDirectory:
3> Copying file from "C:\Temp\TestFastUpToDate\ConsoleApp1\obj\Debug\net8.0\apphost.exe" to "C:\Temp\TestFastUpToDate\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.exe".
3>Target CopyFilesToOutputDirectory:
3> Copying file from "C:\Temp\TestFastUpToDate\ConsoleApp1\obj\Debug\net8.0\ConsoleApp1.dll" to "C:\Temp\TestFastUpToDate\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.dll".
3> ConsoleApp1 -> C:\Temp\TestFastUpToDate\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.dll
3> Copying file from "C:\Temp\TestFastUpToDate\ConsoleApp1\obj\Debug\net8.0\ConsoleApp1.pdb" to "C:\Temp\TestFastUpToDate\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.pdb".
3>
3>Build succeeded.
3> 0 Warning(s)
3> 0 Error(s)
3>
3>Time Elapsed 00:00:00.25
========== Build: 1 succeeded, 0 failed, 2 up-to-date, 0 skipped ==========
========== Build completed at 14:50 and took 00.367 seconds ==========
```
Then when pressing `Build -> Build Solution` again, we get this FastUpToDate build:
```
Build started at 14:51...
1>FastUpToDate: Build acceleration is enabled for this project via a feature flag. See "Tools | Options | Environment | Preview Features" to control this setting. See https://aka.ms/vs-build-acceleration. (ClassLibrary2)
1>FastUpToDate: Comparing timestamps of inputs and outputs: (ClassLibrary2)
1>FastUpToDate: No inputs are newer than earliest output 'C:\Temp\TestFastUpToDate\ClassLibrary2\obj\Debug\net8.0\ClassLibrary2.pdb' (2024-03-12 14:50:08.069). Newest input is 'C:\Temp\TestFastUpToDate\ClassLibrary2\obj\ClassLibrary2.csproj.nuget.g.targets' (2024-03-12 14:48:10.754). (ClassLibrary2)
1>FastUpToDate: Project is up-to-date. (ClassLibrary2)
2>FastUpToDate: Build acceleration is enabled for this project via a feature flag. See "Tools | Options | Environment | Preview Features" to control this setting. See https://aka.ms/vs-build-acceleration. (ClassLibrary1)
2>FastUpToDate: Comparing timestamps of inputs and outputs: (ClassLibrary1)
2>FastUpToDate: No inputs are newer than earliest output 'C:\Temp\TestFastUpToDate\ClassLibrary1\obj\Debug\net8.0\ClassLibrary1.pdb' (2024-03-12 14:50:20.040). Newest input is 'C:\Temp\TestFastUpToDate\ClassLibrary2\bin\Debug\net8.0\ClassLibrary2.dll' (2024-03-12 14:50:08.070). (ClassLibrary1)
2>FastUpToDate: Project is up-to-date. (ClassLibrary1)
3>FastUpToDate: Build acceleration is enabled for this project via a feature flag. See "Tools | Options | Environment | Preview Features" to control this setting. See https://aka.ms/vs-build-acceleration. (ConsoleApp1)
3>FastUpToDate: Comparing timestamps of inputs and outputs: (ConsoleApp1)
3>FastUpToDate: No inputs are newer than earliest output 'C:\Temp\TestFastUpToDate\ConsoleApp1\obj\Debug\net8.0\ConsoleApp1.pdb' (2024-03-12 14:50:08.592). Newest input is 'C:\Temp\TestFastUpToDate\ClassLibrary1\obj\Debug\net8.0\ref\ClassLibrary1.dll' (2024-03-12 14:50:08.101). (ConsoleApp1)
3>FastUpToDate: Copying 1 files to accelerate build (https://aka.ms/vs-build-acceleration): (ConsoleApp1)
3>FastUpToDate: From 'C:\Temp\TestFastUpToDate\ClassLibrary2\bin\Debug\net8.0\ClassLibrary2.dll' to 'C:\Temp\TestFastUpToDate\ConsoleApp1\bin\Debug\net8.0\ClassLibrary2.dll'. (ConsoleApp1)
3>FastUpToDate: Build acceleration copied 1 files. (ConsoleApp1)
3>FastUpToDate: Project is up-to-date. (ConsoleApp1)
========== Build: 0 succeeded, 0 failed, 3 up-to-date, 0 skipped ==========
========== Build completed at 14:51 and took 00.042 seconds ==========
Visual Studio accelerated 1 project(s), copying 1 file(s). See https://aka.ms/vs-build-acceleration.
```
_Note in the FastUpToDate build, we have an extra file copy of ClassLibrary2.dll to the output of ConsoleApp1.
This is the inconsistent behaviour between FastUpToDate and normal build._
## User Impact
The default setting for build acceleration changed in VS 17.9 (according to the last internal VS check-ins referenced in this issue). For our projects this update turned out to be a surprising and hard-to-track-down breaking change. (Even though the previous comment suggests it changed for "sdk style (.NET core) projects", our sdk style (.NET Framework)" project was affected.) The default changed with no indication in the Visual Studio release notes.
The behavior of the "FastUpToDate" build is different to the normal MSBuild in that additional transitive dependencies are copied to the output, where MSBuild would not copy these files. With build acceleration on, the extra files will be copied to output when either doing "Build" or starting to debug, which both trigger the "FastUpToDate" build and hence the extra copy. When doing a "Rebuild" or building after changes, the real MSBuild build is run, which does not copy the extra files. This is even more confusing, since extra files will remain in the output until manually deleted ("Clean" runs MSBuild, which will not remove the extra files).
Whether the extra files should be there or not is another matter, but having these two incompatible build behaviors is very confusing. In our case these files were not used, but broke things at runtime.
There is an easy workaround for the problem - just disable the build acceleration for the project. In our case we were also able to remove the unneeded assembly references, but that might not always be the case.
Contributor guide
Assessment
This issue has not been assessed yet.