Skip to content

Commit a65dcb6

Browse files
authored
Merge pull request #9454 from drewnoakes/fix-9001-duplicate-copy-items-with-ba
Build Acceleration handles duplicate copy items
2 parents 7ee361d + 637ff57 commit a65dcb6

21 files changed

+292
-89
lines changed

docs/build-acceleration.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,11 @@ Build acceleration runs with the FUTDC, and outputs details of its operation in
9292
> Tools | Options | Projects and Solutions | SDK-Style Projects
9393
9494
_Traditional view:_
95+
9596
<img src="repo/images/options.png" width="528" alt="SDK-style project options, in the legacy settings view">
9697

9798
_Unified settings view:_
99+
98100
<img src="repo/images/options-unified.png" width="528" alt="SDK-style project options, in the modern unified settings view">
99101

100102
Setting _Logging Level_ to a value other than `None` results in messages prefixed with `FastUpToDate:` in Visual Studio's build output.
@@ -140,6 +142,12 @@ Looking through the build output with the following points in mind:
140142
141143
Then any project that references the indicated project (directly or transitively) cannot be accelerated. This can happen if the mentioned project uses the legacy `.csproj` format, or for any other project system within Visual Studio that doesn't support build acceleration. Currently only .NET SDK-style projects (loaded with the project system from this GitHub repository) provide the needed data.
142144

145+
- ⛔ If you see:
146+
147+
> Build acceleration is not available for this project because it copies duplicate files to the output directory: '<path1>', '<path2>'
148+
149+
Then multiple projects want to copy the same file to the output directory. Currently, Build Acceleration does not attempt to discover which of these source files should win. Instead, when this situation occurs, Build Acceleration is disabled.
150+
143151
- ⛔ If you see:
144152

145153
> This project has enabled build acceleration, but not all referenced projects produce a reference assembly. Ensure projects producing the following outputs have the 'ProduceReferenceAssembly' MSBuild property set to 'true': '&lt;path1&gt;', '&lt;path2&gt;'.

spelling.dic

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
appsettings
12
args
23
async
34
awaiter
@@ -15,6 +16,7 @@ dirs
1516
docdata
1617
enum
1718
enums
19+
fsproj
1820
fsscript
1921
func
2022
futdcache
@@ -44,6 +46,7 @@ netcoreapp
4446
nuget
4547
pbstr
4648
phier
49+
pkgdef
4750
prev
4851
proj
4952
projectsystem

src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/UpToDate/BuildUpToDateCheck.cs

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -955,7 +955,7 @@ private async Task<bool> IsUpToDateInternalAsync(
955955
// The subscription object calls GetLatestVersionAsync for project data, which can take a while.
956956
// We separate out the wait time from the overall time, so we can more easily identify when the
957957
// wait is long, versus the check's actual execution time.
958-
var waitTime = sw.Elapsed;
958+
TimeSpan waitTime = sw.Elapsed;
959959

960960
token.ThrowIfCancellationRequested();
961961

@@ -1077,7 +1077,7 @@ private async Task<bool> IsUpToDateInternalAsync(
10771077
// We check timestamps of whatever items we can find, but only perform acceleration when the full set is available.
10781078
CopyItemsResult copyInfo = _copyItemAggregator.TryGatherCopyItemsForProject(implicitState.ProjectTargetPath, logger);
10791079

1080-
bool? isBuildAccelerationEnabled = await IsBuildAccelerationEnabledAsync(copyInfo.IsComplete, implicitState);
1080+
bool? isBuildAccelerationEnabled = await IsBuildAccelerationEnabledAsync(copyInfo.IsComplete, copyInfo.DuplicateCopyItemRelativeTargetPaths, implicitState);
10811081

10821082
var configuredFileSystemOperations = new ConfiguredFileSystemOperationAggregator(fileSystemOperations, isBuildAccelerationEnabled, copyInfo.TargetsWithoutReferenceAssemblies);
10831083

@@ -1150,13 +1150,14 @@ private async Task<bool> IsUpToDateInternalAsync(
11501150
_lastCopyTargetsFromThisProject = copyItemPaths;
11511151
}
11521152

1153-
async ValueTask<bool?> IsBuildAccelerationEnabledAsync(bool isCopyItemsComplete, UpToDateCheckImplicitConfiguredInput implicitState)
1153+
async ValueTask<bool?> IsBuildAccelerationEnabledAsync(bool isCopyItemsComplete, IReadOnlyList<string>? duplicateCopyItemRelativeTargetPaths, UpToDateCheckImplicitConfiguredInput implicitState)
11541154
{
11551155
// Build acceleration requires:
11561156
//
11571157
// 1. being enabled, either in the project or via feature flags, and
11581158
// 2. having a full set of copy items, and
1159-
// 3. not having any project references known to be incompatible with Build Acceleration.
1159+
// 3. not having any project references known to be incompatible with Build Acceleration, and
1160+
// 4. not having any duplicate copy items that would overwrite one another in the output directory (due to ordering issues).
11601161
//
11611162
// Being explicitly disabled in the project overrides any feature flag.
11621163

@@ -1180,11 +1181,13 @@ private async Task<bool> IsUpToDateInternalAsync(
11801181

11811182
bool isEnabled;
11821183

1183-
if (isEnabledInProject is null)
1184+
if (isEnabledInProject is bool b)
11841185
{
1185-
// No value has been specified in the project. Look to the feature flag to determine
1186-
// the default behavior in this case.
1187-
1186+
isEnabled = b;
1187+
}
1188+
else
1189+
{
1190+
// No value has been specified in the project. Query the environment to decide (e.g. feature flag).
11881191
if (await _projectSystemOptions.IsBuildAccelerationEnabledByDefaultAsync(cancellationToken))
11891192
{
11901193
// The user has opted-in via feature flag. Set this to true and carry on with further checks.
@@ -1193,32 +1196,32 @@ private async Task<bool> IsUpToDateInternalAsync(
11931196
}
11941197
else
11951198
{
1196-
logger.Verbose(nameof(VSResources.FUTD_BuildAccelerationIsNotEnabledForThisProject));
1199+
logger.Info(nameof(VSResources.FUTD_BuildAccelerationIsNotEnabledForThisProject));
11971200
return null;
11981201
}
11991202
}
1200-
else
1201-
{
1202-
isEnabled = isEnabledInProject.Value;
1203-
}
12041203

12051204
if (isEnabled)
12061205
{
1207-
if (isCopyItemsComplete)
1206+
if (!isCopyItemsComplete)
12081207
{
1209-
if (isEnabledInProject is not null)
1210-
{
1211-
// Don't log if isEnabledInProject is null, as we already log that status above
1212-
logger.Info(nameof(VSResources.FUTD_BuildAccelerationEnabledViaProperty));
1213-
}
1214-
1215-
return true;
1208+
logger.Info(nameof(VSResources.FUTD_AccelerationDisabledCopyItemsIncomplete));
1209+
return false;
12161210
}
1217-
else
1211+
1212+
if (duplicateCopyItemRelativeTargetPaths is not null)
12181213
{
1219-
logger.Info(nameof(VSResources.FUTD_AccelerationDisabledCopyItemsIncomplete));
1214+
logger.Info(nameof(VSResources.FUTD_AccelerationDisabledDuplicateCopyItemsIncomplete_1), string.Join(", ", duplicateCopyItemRelativeTargetPaths.Select(path => $"'{path}'")));
12201215
return false;
12211216
}
1217+
1218+
if (isEnabledInProject is not null)
1219+
{
1220+
// Don't log if isEnabledInProject is null, as we already log that status above.
1221+
logger.Info(nameof(VSResources.FUTD_BuildAccelerationEnabledViaProperty));
1222+
}
1223+
1224+
return true;
12221225
}
12231226
else
12241227
{
@@ -1235,13 +1238,9 @@ public Task<bool> IsUpToDateCheckEnabledAsync(CancellationToken cancellationToke
12351238
return _projectSystemOptions.GetIsFastUpToDateCheckEnabledAsync(cancellationToken);
12361239
}
12371240

1238-
internal readonly struct TestAccessor
1241+
internal readonly struct TestAccessor(BuildUpToDateCheck check)
12391242
{
1240-
private readonly BuildUpToDateCheck _check;
1241-
1242-
public TestAccessor(BuildUpToDateCheck check) => _check = check;
1243-
1244-
public void SetSubscription(ISubscription subscription) => _check._subscription = subscription;
1243+
public void SetSubscription(ISubscription subscription) => check._subscription = subscription;
12451244
}
12461245

12471246
/// <summary>For unit testing only.</summary>

src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/UpToDate/CopyItemAggregator.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public CopyItemsResult TryGatherCopyItemsForProject(string targetPath, BuildUpTo
2727

2828
// The queue of projects yet to be visited.
2929
Queue<string> frontier = new();
30+
31+
// Start with the requested project.
3032
frontier.Enqueue(targetPath);
3133

3234
// If we find a reference to a project that did not call SetProjectData then we will not know the
@@ -59,6 +61,10 @@ public CopyItemsResult TryGatherCopyItemsForProject(string targetPath, BuildUpTo
5961

6062
if (!_projectData.TryGetValue(project, out ProjectCopyData data))
6163
{
64+
// We don't have a list of project references for this project, so we cannot continue
65+
// walking the project reference tree. As we might not know all possible copy items,
66+
// we disable build acceleration. Note that we still walk the rest of the tree in order
67+
// to detect copy items, so that we can decide whether the project is up to date.
6268
logger.Verbose(nameof(VSResources.FUTDC_AccelerationDataMissingForProject_1), project);
6369
isComplete = false;
6470
continue;
@@ -67,24 +73,25 @@ public CopyItemsResult TryGatherCopyItemsForProject(string targetPath, BuildUpTo
6773
if (!data.ProduceReferenceAssembly && project != targetPath)
6874
{
6975
// One of the referenced projects does not produce a reference assembly.
70-
referencesNotProducingReferenceAssembly ??= new();
76+
referencesNotProducingReferenceAssembly ??= [];
7177
referencesNotProducingReferenceAssembly.Add(data.TargetPath);
7278
}
7379

80+
// Breadth-first traversal of the tree.
7481
foreach (string referencedProjectTargetPath in data.ReferencedProjectTargetPaths)
7582
{
7683
frontier.Enqueue(referencedProjectTargetPath);
7784
}
7885

7986
if (!data.CopyItems.IsEmpty)
8087
{
81-
contributingProjects ??= new();
88+
contributingProjects ??= [];
8289
contributingProjects.Add(data);
8390
}
8491
}
8592
}
8693

87-
return new(GenerateCopyItems(), isComplete, referencesNotProducingReferenceAssembly);
94+
return new(isComplete, GenerateCopyItems(), GetDuplicateCopyItems(), referencesNotProducingReferenceAssembly);
8895

8996
IEnumerable<(string Path, ImmutableArray<CopyItem> CopyItems)> GenerateCopyItems()
9097
{
@@ -98,5 +105,29 @@ public CopyItemsResult TryGatherCopyItemsForProject(string targetPath, BuildUpTo
98105
yield return (contributingProject.ProjectFullPath ?? contributingProject.TargetPath, contributingProject.CopyItems);
99106
}
100107
}
108+
109+
IReadOnlyList<string>? GetDuplicateCopyItems()
110+
{
111+
HashSet<string>? duplicates = null;
112+
113+
if (contributingProjects is not null)
114+
{
115+
HashSet<string> targetPaths = new(StringComparers.Paths);
116+
117+
foreach (ProjectCopyData contributingProject in contributingProjects)
118+
{
119+
foreach (CopyItem copyItem in contributingProject.CopyItems)
120+
{
121+
if (!targetPaths.Add(copyItem.RelativeTargetPath))
122+
{
123+
duplicates ??= new(StringComparers.Paths);
124+
duplicates.Add(copyItem.RelativeTargetPath);
125+
}
126+
}
127+
}
128+
}
129+
130+
return duplicates?.ToList();
131+
}
101132
}
102133
}

src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/UpToDate/ICopyItemAggregator.cs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,32 @@ namespace Microsoft.VisualStudio.ProjectSystem.VS.UpToDate;
99
internal interface ICopyItemAggregator
1010
{
1111
/// <summary>
12-
/// Stores the set of copy items produced by the calling project, and modelled
13-
/// in <paramref name="projectCopyData"/>.
12+
/// Integrates data from a project, for use within <see cref="TryGatherCopyItemsForProject"/>.
1413
/// </summary>
15-
/// <param name="projectCopyData">The set of items to copy, and some details about the provider project.</param>
14+
/// <param name="projectCopyData">All necessary data about the project that's providing the data.</param>
1615
void SetProjectData(ProjectCopyData projectCopyData);
1716

1817
/// <summary>
1918
/// Walks the project reference graph in order to obtain all reachable copy items
2019
/// from the project identified by <paramref name="targetPath"/>.
2120
/// </summary>
2221
/// <remarks>
22+
/// <para>
2323
/// We use the <c>TargetPath</c> property to identify projects, as that path takes
24-
/// other dimensions (such as target framework) into account.
24+
/// other dimensions (such as target framework, platform, etc.) into account when projects
25+
/// have multiple configurations.
26+
/// </para>
27+
/// <para>
28+
/// When multiple copy items want to write to the same relative output path, the set of duplicate
29+
/// items is written to <see cref="CopyItemsResult.DuplicateCopyItemRelativeTargetPaths"/>.
30+
/// Build Acceleration disables itself when items exist in this collection, as it does not
31+
/// attempt to reproduce the full behaviour of MSBuild for this scenario.
32+
/// </para>
33+
/// <para>
34+
/// If we find a reference to a project that did not call <see cref="SetProjectData"/> then
35+
/// the returned <see cref="CopyItemsResult.IsComplete"/> will be <see langword="false"/>.
36+
/// Build Acceleration disables itself when copy items from a reachable project is unavailable.
37+
/// </para>
2538
/// </remarks>
2639
/// <param name="targetPath">The target path of the project to query from.</param>
2740
/// <param name="logger">An object for writing log messages.</param>
@@ -33,15 +46,23 @@ internal interface ICopyItemAggregator
3346
/// Results of gathering the items that must be copied as part of a project's build
3447
/// by <see cref="ICopyItemAggregator.TryGatherCopyItemsForProject(string, BuildUpToDateCheck.Log)"/>.
3548
/// </summary>
36-
/// <param name="ItemsByProject">A sequence of items by project, that are reachable from the current project</param>
3749
/// <param name="IsComplete">Indicates whether we have items from all reachable projects.</param>
50+
/// <param name="ItemsByProject">
51+
/// A sequence of items by project, that are reachable from the current project. The path is that
52+
/// of the project file, such as <c>c:\repos\MyProject\MyProject.csproj</c>.
53+
/// </param>
54+
/// <param name="DuplicateCopyItemRelativeTargetPaths">
55+
/// A list of relative target paths for which more than one project produces an item, or <see langword="null"/> if
56+
/// no duplicates exists.
57+
/// </param>
3858
/// <param name="TargetsWithoutReferenceAssemblies">
3959
/// A list of target paths for projects that do not produce reference assemblies, or <see langword="null"/> if
4060
/// all reachable projects do in fact produce reference assemblies.
4161
/// </param>
4262
internal record struct CopyItemsResult(
43-
IEnumerable<(string Path, ImmutableArray<CopyItem> CopyItems)> ItemsByProject,
4463
bool IsComplete,
64+
IEnumerable<(string Path, ImmutableArray<CopyItem> CopyItems)> ItemsByProject,
65+
IReadOnlyList<string>? DuplicateCopyItemRelativeTargetPaths,
4566
IReadOnlyList<string>? TargetsWithoutReferenceAssemblies);
4667

4768
/// <summary>
@@ -59,5 +80,5 @@ internal record struct ProjectCopyData(
5980
ImmutableArray<CopyItem> CopyItems,
6081
ImmutableArray<string> ReferencedProjectTargetPaths)
6182
{
62-
public bool IsDefault => CopyItems.IsDefault;
83+
public readonly bool IsDefault => CopyItems.IsDefault;
6384
}

src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/VSResources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/VSResources.resx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,10 @@ In order to debug this project, add an executable project to this solution which
543543
<data name="FUTD_AccelerationDisabledCopyItemsIncomplete" xml:space="preserve">
544544
<value>Build acceleration is not available for this project because not all transitively referenced projects have provided acceleration data.</value>
545545
</data>
546+
<data name="FUTD_AccelerationDisabledDuplicateCopyItemsIncomplete_1" xml:space="preserve">
547+
<value>Build acceleration is not available for this project because it copies duplicate files to the output directory: {0}</value>
548+
<comment>{0} is a list of one or more relative file paths.</comment>
549+
</data>
546550
<data name="FUTDC_AccelerationDataMissingForProject_1" xml:space="preserve">
547551
<value>Build acceleration data is unavailable for project with target '{0}'. See https://aka.ms/vs-build-acceleration.</value>
548552
<comment>{0} is a file path.</comment>

src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/xlf/VSResources.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/xlf/VSResources.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)