Skip to content
Merged
55 changes: 55 additions & 0 deletions src/Build.UnitTests/BinaryLogger_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,61 @@ public void BinaryLoggerShouldEmbedFilesViaTaskOutput()
$"Embedded files: {string.Join(",", zipArchive.Entries)}");
}

[Fact]
public void BinaryLoggerShouldEmbedFilesWithRelativePathFromChildProjects()
{
// Repro for https://github.com/dotnet/msbuild/issues/13789: EmbedInBinlog items with a
// relative Include path emitted by child projects must still be embedded. The binary
// logger runs on the entrypoint node, so a relative path coming from a child project
// (built on another node) has to be resolved against that child's directory, otherwise
// it is silently dropped. A parallel multi-node build forces the items onto worker nodes.
//
// The generated file is written via an absolute path so its creation is deterministic;
// only the EmbedInBinlog Include uses a relative path, which is the behavior under test.
TransientTestFolder rootFolder = _env.CreateFolder();

Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Resolved
const string childProjectContents = @"
<Project>
<Target Name=""Build"">
<WriteLinesToFile File=""$(MSBuildProjectDirectory)\generated.txt"" Lines=""generated"" Overwrite=""true"" />
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Resolved
<CreateItem Include=""generated.txt"">
<Output TaskParameter=""Include"" ItemName=""EmbedInBinlog"" />
</CreateItem>
</Target>
</Project>";

string childADir = Directory.CreateDirectory(Path.Combine(rootFolder.Path, "ChildA")).FullName;
string childBDir = Directory.CreateDirectory(Path.Combine(rootFolder.Path, "ChildB")).FullName;
File.WriteAllText(Path.Combine(childADir, "ChildA.proj"), childProjectContents);
File.WriteAllText(Path.Combine(childBDir, "ChildB.proj"), childProjectContents);

const string parentProjectContents = @"
<Project>
<ItemGroup>
<ChildProject Include=""ChildA\ChildA.proj"" />
<ChildProject Include=""ChildB\ChildB.proj"" />
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Resolved
Comment thread
AlesProkop marked this conversation as resolved.
Resolved
</ItemGroup>
<Target Name=""Build"">
<MSBuild Projects=""@(ChildProject)"" Targets=""Build"" BuildInParallel=""true"" />
</Target>
</Project>";
string parentProjectPath = Path.Combine(rootFolder.Path, "build.proj");
File.WriteAllText(parentProjectPath, parentProjectContents);

RunnerUtilities.ExecMSBuild(
$"\"{parentProjectPath}\" -m:2 -bl:\"{_logFile};ProjectImports=ZipFile\"",
out bool success);
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Resolved
success.ShouldBeTrue();

var projectImportsZipPath = Path.ChangeExtension(_logFile, ".ProjectImports.zip");
using var fileStream = new FileStream(projectImportsZipPath, FileMode.Open);
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read);

// Both child projects' generated files must be embedded, not just the entrypoint's.
int generatedFileCount = zipArchive.Entries.Count(zE => zE.Name.EndsWith("generated.txt", StringComparison.Ordinal));
generatedFileCount.ShouldBe(2, $"Embedded files: {string.Join(",", zipArchive.Entries)}");
}

[RequiresSymbolicLinksFact]
public void BinaryLoggerShouldEmbedSymlinkFilesViaTaskOutput()
{
Expand Down
11 changes: 11 additions & 0 deletions src/Build/BackEnd/Components/Logging/LoggingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,17 @@ internal void InjectNonSerializedData(LogMessagePacket loggingPacket)
_projectFileMap[projectStartedEventArgs.BuildEventContext.ProjectContextId] = projectStartedEventArgs.ProjectFile;
}
}
else if (loggingPacket.NodeBuildEvent.Value.Value is TaskParameterEventArgs taskParameterEventArgs &&
string.Equals(taskParameterEventArgs.ItemType, ItemTypeNames.EmbedInBinlog, StringComparison.OrdinalIgnoreCase) &&
string.IsNullOrEmpty(taskParameterEventArgs.ProjectFile) &&
taskParameterEventArgs.BuildEventContext != null &&
taskParameterEventArgs.BuildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId)
{
// ProjectFile is not serialized across nodes for perf reasons, but the binary logger needs it to
// resolve relative EmbedInBinlog item paths against the originating project's directory (#13789).
// Restore it from the project context map, mirroring what LogBuildEvent does for in-proc events.
taskParameterEventArgs.ProjectFile = GetAndVerifyProjectFileFromContext(taskParameterEventArgs, allowCacheMiss: true);
}
}
}

Expand Down
50 changes: 39 additions & 11 deletions src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ private BinaryLogRecordKind Write(ProjectEvaluationFinishedEventArgs e)

WriteProperties(e.Properties);

WriteProjectItems(e.Items);
WriteProjectItems(e.Items, e.ProjectFile);

var result = e.ProfilerResult;
Write(result.HasValue);
Expand Down Expand Up @@ -432,7 +432,7 @@ private BinaryLogRecordKind Write(ProjectStartedEventArgs e)

WriteProperties(e.Properties);

WriteProjectItems(e.Items);
WriteProjectItems(e.Items, e.ProjectFile);

return BinaryLogRecordKind.ProjectStarted;
}
Expand Down Expand Up @@ -655,7 +655,7 @@ private BinaryLogRecordKind Write(TaskParameterEventArgs e)
if (e.Kind == TaskParameterMessageKind.AddItem
|| e.Kind == TaskParameterMessageKind.TaskOutput)
{
CheckForFilesToEmbed(e.ItemType, e.Items);
CheckForFilesToEmbed(e.ItemType, e.Items, e.ProjectFile);
}
return BinaryLogRecordKind.TaskParameter;
}
Expand Down Expand Up @@ -982,7 +982,7 @@ private void WriteTaskItemList(IEnumerable items, bool writeMetadata = true)
reusableItemsList.Clear();
}

private void WriteProjectItems(IEnumerable items)
private void WriteProjectItems(IEnumerable items, string projectFile)
{
if (items == null)
{
Expand All @@ -999,7 +999,7 @@ private void WriteProjectItems(IEnumerable items)
{
WriteDeduplicatedString(itemType);
WriteTaskItemList(itemList);
CheckForFilesToEmbed(itemType, itemList);
CheckForFilesToEmbed(itemType, itemList, projectFile);
});

// signal the end
Expand All @@ -1012,7 +1012,7 @@ private void WriteProjectItems(IEnumerable items)
{
WriteDeduplicatedString(itemType);
WriteTaskItemList(itemList);
CheckForFilesToEmbed(itemType, itemList);
CheckForFilesToEmbed(itemType, itemList, projectFile);
});

// signal the end
Expand Down Expand Up @@ -1040,7 +1040,7 @@ private void WriteProjectItems(IEnumerable items)
{
WriteDeduplicatedString(currentItemType);
WriteTaskItemList(reusableProjectItemList);
CheckForFilesToEmbed(currentItemType, reusableProjectItemList);
CheckForFilesToEmbed(currentItemType, reusableProjectItemList, projectFile);
reusableProjectItemList.Clear();
}

Expand All @@ -1053,7 +1053,7 @@ private void WriteProjectItems(IEnumerable items)
{
WriteDeduplicatedString(currentItemType);
WriteTaskItemList(reusableProjectItemList);
CheckForFilesToEmbed(currentItemType, reusableProjectItemList);
CheckForFilesToEmbed(currentItemType, reusableProjectItemList, projectFile);
reusableProjectItemList.Clear();
}

Expand All @@ -1062,7 +1062,7 @@ private void WriteProjectItems(IEnumerable items)
}
}

private void CheckForFilesToEmbed(string itemType, object itemList)
private void CheckForFilesToEmbed(string itemType, object itemList, string projectFile)
{
if (EmbedFile == null ||
!string.Equals(itemType, ItemTypeNames.EmbedInBinlog, StringComparison.OrdinalIgnoreCase) ||
Expand All @@ -1075,15 +1075,43 @@ private void CheckForFilesToEmbed(string itemType, object itemList)
{
if (item is ITaskItem taskItem && !string.IsNullOrEmpty(taskItem.ItemSpec))
{
EmbedFile.Invoke(taskItem.ItemSpec);
EmbedFile.Invoke(ResolveEmbedPath(taskItem.ItemSpec, projectFile));
}
else if (item is string itemSpec && !string.IsNullOrEmpty(itemSpec))
{
EmbedFile.Invoke(itemSpec);
EmbedFile.Invoke(ResolveEmbedPath(itemSpec, projectFile));
}
}
}

/// <summary>
/// Resolves an <c>EmbedInBinlog</c> item spec to an absolute path. Items created on a
/// non-entrypoint node carry an Include path relative to their own project directory, but
/// the binary logger runs on the entrypoint node, so a relative path would otherwise be
/// resolved against the wrong working directory and silently dropped (issue #13789).
/// Resolve it against the project's directory when we have that context.
/// </summary>
private static string ResolveEmbedPath(string itemSpec, string projectFile)
{
if (Path.IsPathRooted(itemSpec) || string.IsNullOrEmpty(projectFile))
{
return itemSpec;
}

try
{
string projectDirectory = Path.GetDirectoryName(projectFile);
return string.IsNullOrEmpty(projectDirectory)
? itemSpec
: Path.Combine(projectDirectory, itemSpec);
}
catch (ArgumentException)
{
// Invalid characters in the path; fall back to the raw item spec.
return itemSpec;
}
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Resolved
}
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Resolved

private void Write(ITaskItem item, bool writeMetadata = true)
{
WriteDeduplicatedString(item.ItemSpec);
Expand Down
3 changes: 2 additions & 1 deletion src/Framework/MSBuildConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ internal static class ItemTypeNames
internal const string ProjectCachePlugin = nameof(ProjectCachePlugin);

/// <summary>
/// Embed specified files in the binary log
/// Embed specified files in the binary log. Item specs should be absolute paths; relative
/// paths are resolved against the directory of the project that declares the item.
/// </summary>
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Resolved
internal const string EmbedInBinlog = nameof(EmbedInBinlog);
}
Expand Down
Loading