Skip to content

Commit

Permalink
Combine distributed logger logic (PR comment) (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
erikmav authored Nov 1, 2023
1 parent 9de0055 commit 4ebc8ac
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 324 deletions.
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The following warnings are generated by this package:
## How to use
Add a package reference to the [ReferenceTrimmer](https://www.nuget.org/packages/ReferenceTrimmer) package in your projects, or as a common package reference in the repo's [`Directory.Packages.props`](./Directory.Build.props).

If you're using [Central Package Management](https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management), you can it as a `GlobalPackageReference` in your `Directory.Packages.props` to apply it to the entire repo.
If you're using [Central Package Management](https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management), you can use it as a `GlobalPackageReference` in your `Directory.Packages.props` to apply it to the entire repo.

```xml
<ItemGroup>
Expand Down Expand Up @@ -114,13 +114,7 @@ To turn off a rule on a specific project or package reference, add the relevant
## Configuration
`$(EnableReferenceTrimmer)` - Controls whether the build logic should run for a given project. Defaults to `true`.

`$(ReferenceTrimmerEnableVcxproj)` - Controls whether MSVC link flags are set up to print out unused libraries and delay-load DLLs. Set to `false` to disable.

## Disabling a rule on a reference
To turn off a rule on a specific project or package reference, add the relevant RTxxxx code to a NoWarn metadata attribute. For example:

```xml
<ProjectReference Include="../Other/Project.csproj" NoWarn="RT0002" />
`$(ReferenceTrimmerEnableVcxproj)` - Controls whether MSVC link flags are set up to print out unused libraries and delay-load DLLs. Defaults to `true`.

## How does it work?

Expand Down
2 changes: 1 addition & 1 deletion src/Loggers/MSVC/CentralLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public sealed class CentralLogger : Logger
private bool _firstEvent = true;
private string? _jsonLogFilePath;

internal const string JsonLogFileName = UnusedLibsLogger.HelpKeyword + ".json.log";
internal const string JsonLogFileName = ForwardingLogger.HelpKeyword + ".json.log";

/// <inheritdoc />
public override void Initialize(IEventSource eventSource)
Expand Down
231 changes: 228 additions & 3 deletions src/Loggers/MSVC/ForwardingLogger.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Concurrent;
using System.Text;
using Microsoft.Build.Framework;

namespace ReferenceTrimmer.Loggers.MSVC;
Expand All @@ -7,7 +9,51 @@ namespace ReferenceTrimmer.Loggers.MSVC;
/// </summary>
public sealed class ForwardingLogger : IForwardingLogger
{
private UnusedLibsLogger? _logger;
private const string LinkTaskName = "Link";

// Default library list in $(CoreLibraryDependencies) used as a default list for $(AdditionalDependencies)
// in the MSVC MSBuild SDK. To update with current lib list:
// findstr /s CoreLibraryDependencies "\Program Files"\*props
// Ignores WindowsApp.lib for the WindowsAppContainer case.
private static readonly HashSet<string> DefaultWin32DllImportLibraries = new(StringComparer.OrdinalIgnoreCase)
{
"advapi32.lib",
"comdlg32.lib",
"gdi32.lib",
"kernel32.lib",
"odbc32.lib",
"odbccp32.lib",
"ole32.lib",
"oleaut32.lib",
"shell32.lib",
"user32.lib",
"uuid.lib",
"winspool.lib",
};

private enum State
{
LinkStarted,
UnusedLibsStarted,
UnusedLibsEnded,
}

private enum LibType
{
DefaultImportLib,
PackageOrDependencyLib,
}

private sealed class ProjectStateLibs
{
public State ProjectState { get; set; }
public SortedSet<string> UnusedProjectLibPaths { get; } = new(StringComparer.OrdinalIgnoreCase);
}

private IEventSource? _eventSource;
private readonly ConcurrentDictionary<string, ProjectStateLibs> _projects = new(StringComparer.OrdinalIgnoreCase);

public const string HelpKeyword = "ReferenceTrimmerUnusedMSVCLibraries";

/// <summary>
/// Gets or sets the level of detail to show in the event log.
Expand Down Expand Up @@ -44,14 +90,193 @@ public void Initialize(IEventSource eventSource, int nodeCount)
/// </summary>
public void Initialize(IEventSource eventSource)
{
_logger = new UnusedLibsLogger(eventSource, BuildEventRedirector!);
_eventSource = eventSource;

eventSource.TaskStarted += OnTaskStarted;
eventSource.TaskFinished += OnTaskFinished;
eventSource.MessageRaised += OnMessageRaised;
}

/// <summary>
/// Called when Engine is done with this logger
/// </summary>
public void Shutdown()
{
_logger?.Dispose();
if (_eventSource is null)
{
return;
}

_eventSource.TaskStarted -= OnTaskStarted;
_eventSource.TaskFinished -= OnTaskFinished;
_eventSource.MessageRaised -= OnMessageRaised;
}

private void OnTaskStarted(object sender, TaskStartedEventArgs e)
{
if (!string.IsNullOrEmpty(e.ProjectFile) && e.TaskName.Equals(LinkTaskName, StringComparison.OrdinalIgnoreCase))
{
_projects[e.ProjectFile] = new ProjectStateLibs { ProjectState = State.LinkStarted };
}
}

private void OnTaskFinished(object sender, TaskFinishedEventArgs e)
{
if (string.IsNullOrEmpty(e.ProjectFile) || e.TaskName != LinkTaskName || !e.Succeeded)
{
return;
}

string projectFilePath = e.ProjectFile;

// Project state present in map if the Link task was detected running in OnTaskStarted.
if (!_projects.TryGetValue(projectFilePath, out ProjectStateLibs projState))
{
return;
}

if (projState.ProjectState is State.UnusedLibsStarted or State.UnusedLibsEnded &&
projState.UnusedProjectLibPaths.Count > 0)
{
// Hack the output JSON format to avoid importing Newtonsoft or System.Text.Json that could collide with
// versions in the current MSBuild process.
var jsonSb = new StringBuilder(256);
jsonSb.AppendLine(" {");
jsonSb.AppendLine($" \"{EscapeJsonChars(projectFilePath)}\": [");

// Classify the libraries.
var defaultImportLibraries = new List<string>();
var otherLibraries = new List<string>();
var implicitlyUsedImportLibraries = new HashSet<string>(DefaultWin32DllImportLibraries, StringComparer.OrdinalIgnoreCase);
bool first = true;
foreach (string libPath in projState.UnusedProjectLibPaths)
{
if (first)
{
first = false;
}
else
{
jsonSb.AppendLine(",");
}

jsonSb.AppendLine(" {")
.AppendLine($" \"LibPath\": \"{EscapeJsonChars(libPath)}\",");

string libFileName = Path.GetFileName(libPath);
LibType type;
if (DefaultWin32DllImportLibraries.Contains(libFileName))
{
defaultImportLibraries.Add(libPath);
implicitlyUsedImportLibraries.Remove(libFileName);
type = LibType.DefaultImportLib;
}
else
{
otherLibraries.Add(libPath);
type = LibType.PackageOrDependencyLib;
}

jsonSb.AppendLine($" \"LibType\": \"{type}\"")
.Append(" }");
}

jsonSb.AppendLine();
jsonSb.AppendLine(" ]");
jsonSb.Append(" }");

var sb = new StringBuilder($" Unused MSVC libraries detected in project {projectFilePath}:", 256);
sb.AppendLine();

if (defaultImportLibraries.Count > 0)
{
var implicitlyUsedImportLibrariesSorted = new List<string>(implicitlyUsedImportLibraries);
implicitlyUsedImportLibrariesSorted.Sort(StringComparer.OrdinalIgnoreCase);
sb.Append(" * Default Windows SDK import libraries:");
if (implicitlyUsedImportLibrariesSorted.Count > 0)
{
sb.AppendLine().Append($" - Libraries needed: {string.Join(";", implicitlyUsedImportLibrariesSorted)} (set in $(AdditionalDependencies) without %(AdditionalDependencies))");
}

sb.AppendLine().Append($" - Unneeded: {string.Join(", ", defaultImportLibraries.Select(Path.GetFileName))} (remove from $(AdditionalDependencies))");
}

if (otherLibraries.Count > 0)
{
sb.AppendLine().Append(
" * Other libraries - if a lib from a project in this repo, remove the related ProjectReference to improve your build dependency graph; " +
"if a package lib, remove from $(AdditionalDependencies):");
foreach (string lib in otherLibraries)
{
sb.AppendLine().Append($" - {lib}");
}
}

BuildEventRedirector!.ForwardEvent(
new UnusedLibsCustomBuildEventArgs(
message: sb.ToString(),
projectFilePath,
jsonSb.ToString()));
}

_projects.TryRemove(projectFilePath, out _);
}

private static string EscapeJsonChars(string str)
{
return str.Replace("\\", "\\\\").Replace("\"", "\\\"");
}

private void OnMessageRaised(object sender, BuildMessageEventArgs e)
{
if (string.IsNullOrEmpty(e.ProjectFile))
{
return;
}

string projectFilePath = e.ProjectFile;

if (!_projects.TryGetValue(projectFilePath, out ProjectStateLibs? projState))
{
return;
}

// The 'Unused libraries' message is at the tail of Link's stdout.
// Once found, assume the rest of the output is the list of unused libs.
switch (projState.ProjectState)
{
case State.LinkStarted:
if (e.Message.IndexOf("Unused libraries:", StringComparison.OrdinalIgnoreCase) != -1)
{
projState.ProjectState = State.UnusedLibsStarted;
}
break;

case State.UnusedLibsStarted:
string lib = e.Message.Trim();
if (lib.Length > 0)
{
try
{
lib = Path.GetFullPath(lib);
}
finally
{
projState.UnusedProjectLibPaths.Add(lib);
}
}
else
{
projState.ProjectState = State.UnusedLibsEnded;
}
break;

// Avoid parsing any other text after the usual empty line emitted after the unused libs list.
// Allow to fall through.
case State.UnusedLibsEnded:

default:
break;
}
}
}
6 changes: 3 additions & 3 deletions src/Loggers/MSVC/UnusedLibsCustomBuildEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace ReferenceTrimmer.Loggers.MSVC;

/// <summary>
/// Passes the unused library information from <see cref="ForwardingLogger"/>/<see cref="CentralLogger"/>
/// to <see cref="UnusedLibsLogger"/>. Note that the MSBuild binary logger will serialize this event in its
/// Passes the unused library information from <see cref="ForwardingLogger"/> to
/// <see cref="CentralLogger"/>. Note that the MSBuild binary logger will serialize this event in its
/// entirety, so to aid complete debugging fully analyzed information is passed here.
/// </summary>
[Serializable]
Expand All @@ -20,7 +20,7 @@ public UnusedLibsCustomBuildEventArgs(
string message,
string projectPath,
string unusedLibraryPathsJson)
: base(message, UnusedLibsLogger.HelpKeyword, senderName: projectPath)
: base(message, ForwardingLogger.HelpKeyword, senderName: projectPath)
{
ProjectPath = projectPath;
UnusedLibraryPathsJson = unusedLibraryPathsJson;
Expand Down
Loading

0 comments on commit 4ebc8ac

Please sign in to comment.