Skip to content

Commit

Permalink
Adds support for reading binary log files
Browse files Browse the repository at this point in the history
  • Loading branch information
daveaglick committed Oct 2, 2018
1 parent 36d4733 commit c80ee21
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 31 deletions.
11 changes: 6 additions & 5 deletions src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static Project AddToWorkspace(this AnalyzerResult analyzerResult, Workspa
{
if(!existingProject.Id.Equals(projectId)
&& _projectReferences.TryGetValue(existingProject.Id, out string[] existingReferences)
&& existingReferences.Contains(analyzerResult.Analyzer.ProjectFile.Path))
&& existingReferences.Contains(analyzerResult.ProjectFilePath))
{
// Add the reference to the existing project
ProjectReference projectReference = new ProjectReference(projectId);
Expand Down Expand Up @@ -105,15 +105,15 @@ public static Project AddToWorkspace(this AnalyzerResult analyzerResult, Workspa

private static ProjectInfo GetProjectInfo(AnalyzerResult analyzerResult, Workspace workspace, ProjectId projectId)
{
string projectName = Path.GetFileNameWithoutExtension(analyzerResult.Analyzer.ProjectFile.Path);
string languageName = GetLanguageName(analyzerResult.Analyzer.ProjectFile.Path);
string projectName = Path.GetFileNameWithoutExtension(analyzerResult.ProjectFilePath);
string languageName = GetLanguageName(analyzerResult.ProjectFilePath);
ProjectInfo projectInfo = ProjectInfo.Create(
projectId,
VersionStamp.Create(),
projectName,
projectName,
languageName,
filePath: analyzerResult.Analyzer.ProjectFile.Path,
filePath: analyzerResult.ProjectFilePath,
outputFilePath: analyzerResult.GetProperty("TargetPath"),
documents: GetDocuments(analyzerResult, projectId),
projectReferences: GetExistingProjectReferences(analyzerResult, workspace),
Expand Down Expand Up @@ -166,9 +166,10 @@ private static IEnumerable<ProjectReference> GetExistingProjectReferences(Analyz

private static IEnumerable<ProjectAnalyzer> GetReferencedAnalyzerProjects(AnalyzerResult analyzerResult) =>
analyzerResult.ProjectReferences
.Select(x => analyzerResult.Analyzer.Manager.Projects.TryGetValue(x, out ProjectAnalyzer a) ? a : null)
.Select(x => analyzerResult.Manager.Projects.TryGetValue(x, out ProjectAnalyzer a) ? a : null)
.Where(x => x != null)
?? Array.Empty<ProjectAnalyzer>();

private static IEnumerable<DocumentInfo> GetDocuments(AnalyzerResult analyzerResult, ProjectId projectId) =>
analyzerResult
.SourceFiles ?.Where(File.Exists)
Expand Down
29 changes: 29 additions & 0 deletions src/Buildalyzer/AnalyzerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Xml.Linq;
using Buildalyzer.Construction;
using Buildalyzer.Environment;
using Buildalyzer.Logging;
using Microsoft.Build.Construction;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using Microsoft.Build.Logging.StructuredLogger;
using Microsoft.Extensions.Logging;

namespace Buildalyzer
Expand Down Expand Up @@ -83,6 +87,31 @@ public void SetEnvironmentVariable(string key, string value)

public ProjectAnalyzer GetProject(string projectFilePath) => GetProject(projectFilePath, null);

/// <summary>
/// Analyzes an MSBuild binary log file.
/// </summary>
/// <param name="binLogPath">The path to the binary log file.</param>
/// <param name="buildLoggers">MSBuild loggers to replay events from the log to.</param>
/// <returns>A dictionary of target frameworks to <see cref="AnalyzerResult"/>.</returns>
public AnalyzerResults Analyze(string binLogPath, IEnumerable<Microsoft.Build.Framework.ILogger> buildLoggers = null)
{
binLogPath = NormalizePath(binLogPath);
if(!File.Exists(binLogPath))
{
throw new ArgumentException($"The path {binLogPath} could not be found.");
}

BinLogReader reader = new BinLogReader();
using (EventProcessor eventProcessor = new EventProcessor(this, null, buildLoggers, reader, true))
{
reader.Replay(binLogPath);
return new AnalyzerResults
{
{ eventProcessor.Results, eventProcessor.OverallSuccess }
};
}
}

private ProjectAnalyzer GetProject(string projectFilePath, ProjectInSolution projectInSolution)
{
if (projectFilePath == null)
Expand Down
23 changes: 19 additions & 4 deletions src/Buildalyzer/AnalyzerResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,32 @@ public class AnalyzerResult
private readonly Guid _projectGuid;
private List<(string, string)> _cscCommandLineArguments;

internal AnalyzerResult(ProjectAnalyzer analyzer)
internal AnalyzerResult(string projectFilePath, AnalyzerManager manager, ProjectAnalyzer analyzer)
{
ProjectFilePath = projectFilePath;
Manager = manager;
Analyzer = analyzer;

string projectGuid = GetProperty(nameof(ProjectGuid));
if(string.IsNullOrEmpty(projectGuid) || !Guid.TryParse(projectGuid, out _projectGuid))
{
_projectGuid = analyzer.ProjectGuid;
_projectGuid = analyzer == null
? GuidUtility.Create(GuidUtility.UrlNamespace, ProjectFilePath)
: analyzer.ProjectGuid;
}
}

/// <summary>
/// The full normalized path to the project file.
/// </summary>
public string ProjectFilePath { get; }

public AnalyzerManager Manager { get; }

/// <summary>
/// Gets the <see cref="ProjectAnalyzer"/> that generated this result
/// or <c>null</c> if the result came from a binary log file.
/// </summary>
public ProjectAnalyzer Analyzer { get; }

public bool Succeeded { get; internal set; }
Expand Down Expand Up @@ -67,7 +82,7 @@ public string GetProperty(string name) =>
?.Where(x => x.Item1 == null
&& !string.Equals(Path.GetFileName(x.Item2), "csc.dll", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(Path.GetFileName(x.Item2), "csc.exe", StringComparison.OrdinalIgnoreCase))
.Select(x => AnalyzerManager.NormalizePath(Path.Combine(Path.GetDirectoryName(Analyzer.ProjectFile.Path), x.Item2)))
.Select(x => AnalyzerManager.NormalizePath(Path.Combine(Path.GetDirectoryName(ProjectFilePath), x.Item2)))
.ToArray() ?? Array.Empty<string>();

public string[] References =>
Expand All @@ -79,7 +94,7 @@ public string GetProperty(string name) =>
public IEnumerable<string> ProjectReferences =>
Items.TryGetValue("ProjectReference", out ProjectItem[] items)
? items.Select(x => AnalyzerManager.NormalizePath(
Path.Combine(Path.GetDirectoryName(Analyzer.ProjectFile.Path), x.ItemSpec)))
Path.Combine(Path.GetDirectoryName(ProjectFilePath), x.ItemSpec)))
: Array.Empty<string>();

internal void ProcessProject(ProjectStartedEventArgs e)
Expand Down
4 changes: 1 addition & 3 deletions src/Buildalyzer/AnalyzerResults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ public class AnalyzerResults : IEnumerable<AnalyzerResult>
private bool? _overallSuccess = null;

public bool OverallSuccess => _overallSuccess.HasValue ? _overallSuccess.Value : false;

internal void Add(AnalyzerResult result) => _results.Add(result.TargetFramework ?? string.Empty, result);


internal void Add(IEnumerable<AnalyzerResult> results, bool overallSuccess)
{
foreach (AnalyzerResult result in results)
Expand Down
1 change: 1 addition & 0 deletions src/Buildalyzer/Buildalyzer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="MSBuild.StructuredLogger" Version="2.0.11" />
<PackageReference Include="MsBuildPipeLogger.Server" Version="1.1.2" />
</ItemGroup>
<ItemGroup>
Expand Down
36 changes: 36 additions & 0 deletions src/Buildalyzer/Logging/BuildEventArgsReaderProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;

namespace Buildalyzer.Logging
{
internal class BuildEventArgsReaderProxy
{
private readonly Func<BuildEventArgs> _read;

public BuildEventArgsReaderProxy(BinaryReader reader)
{
// Use reflection to get the Microsoft.Build.Logging.BuildEventArgsReader.Read() method
object argsReader;
Type buildEventArgsReader = typeof(BinaryLogger).GetTypeInfo().Assembly.GetType("Microsoft.Build.Logging.BuildEventArgsReader");
ConstructorInfo readerCtor = buildEventArgsReader.GetConstructor(new[] { typeof(BinaryReader) });
if (readerCtor != null)
{
argsReader = readerCtor.Invoke(new[] { reader });
}
else
{
readerCtor = buildEventArgsReader.GetConstructor(new[] { typeof(BinaryReader), typeof(int) });
argsReader = readerCtor.Invoke(new object[] { reader, 7 });
}
MethodInfo readMethod = buildEventArgsReader.GetMethod("Read");
_read = (Func<BuildEventArgs>)readMethod.CreateDelegate(typeof(Func<BuildEventArgs>), argsReader);
}

public BuildEventArgs Read() => _read();
}
}
33 changes: 22 additions & 11 deletions src/Buildalyzer/Logging/EventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,34 @@ internal class EventProcessor : IDisposable
{
private readonly Dictionary<string, AnalyzerResult> _results = new Dictionary<string, AnalyzerResult>();
private readonly Stack<AnalyzerResult> _currentResult = new Stack<AnalyzerResult>();
private readonly AnalyzerManager _manager;
private readonly ProjectAnalyzer _analyzer;
private readonly ILogger<EventProcessor> _logger;
private readonly IEnumerable<Microsoft.Build.Framework.ILogger> _loggers;
private readonly IEnumerable<Microsoft.Build.Framework.ILogger> _buildLoggers;
private readonly IEventSource _eventSource;
private readonly bool _analyze;

private string _projectFilePath;

public bool OverallSuccess { get; private set; }

public IEnumerable<AnalyzerResult> Results => _results.Values;

public EventProcessor(ProjectAnalyzer analyzer, IEnumerable<Microsoft.Build.Framework.ILogger> loggers, IEventSource eventSource, bool analyze)
public EventProcessor(AnalyzerManager manager, ProjectAnalyzer analyzer, IEnumerable<Microsoft.Build.Framework.ILogger> buildLoggers, IEventSource eventSource, bool analyze)
{
_manager = manager;
_analyzer = analyzer;
_logger = analyzer.Manager.LoggerFactory?.CreateLogger<EventProcessor>();
_loggers = loggers;
_logger = manager.LoggerFactory?.CreateLogger<EventProcessor>();
_buildLoggers = buildLoggers ?? Array.Empty<Microsoft.Build.Framework.ILogger>();
_eventSource = eventSource;
_analyze = analyze;

_projectFilePath = _analyzer?.ProjectFile.Path;

// Initialize the loggers
// TODO: Figure out what to do with loggers: don't filter if using loggers, what about console (use stdout?)
foreach (Microsoft.Build.Framework.ILogger logger in loggers)
foreach (Microsoft.Build.Framework.ILogger buildLogger in _buildLoggers)
{
logger.Initialize(eventSource);
buildLogger.Initialize(eventSource);
}

// Send events to the tree constructor
Expand All @@ -52,8 +57,14 @@ public EventProcessor(ProjectAnalyzer analyzer, IEnumerable<Microsoft.Build.Fram

private void ProjectStarted(object sender, ProjectStartedEventArgs e)
{
// If we're not using an analyzer (I.e., from a binary log) and this is the first project file path we've seen, then it's the primary
if(_projectFilePath == null)
{
_projectFilePath = AnalyzerManager.NormalizePath(e.ProjectFile);
}

// Make sure this is the same project, nested MSBuild tasks may have spawned additional builds of other projects
if (AnalyzerManager.NormalizePath(e.ProjectFile) == _analyzer.ProjectFile.Path)
if (AnalyzerManager.NormalizePath(e.ProjectFile) == _projectFilePath)
{
// Get the TFM for this project
string tfm = e.Properties.Cast<DictionaryEntry>()
Expand All @@ -62,7 +73,7 @@ private void ProjectStarted(object sender, ProjectStartedEventArgs e)
{
if (!_results.TryGetValue(tfm, out AnalyzerResult result))
{
result = new AnalyzerResult(_analyzer);
result = new AnalyzerResult(_projectFilePath, _manager, _analyzer);
_results[tfm] = result;
}
result.ProcessProject(e);
Expand Down Expand Up @@ -118,9 +129,9 @@ public void Dispose()
}

// Need to release the loggers in case they get used again (I.e., Restore followed by Clean;Build)
foreach (Microsoft.Build.Framework.ILogger logger in _loggers)
foreach (Microsoft.Build.Framework.ILogger buildLogger in _buildLoggers)
{
logger.Shutdown();
buildLogger.Shutdown();
}
}
}
Expand Down
13 changes: 6 additions & 7 deletions src/Buildalyzer/ProjectAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Buildalyzer
{
public class ProjectAnalyzer
{
private readonly List<ILogger> _loggers = new List<ILogger>();
private readonly List<ILogger> _buildLoggers = new List<ILogger>();

// Project-specific global properties and environment variables
private readonly ConcurrentDictionary<string, string> _globalProperties = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
Expand Down Expand Up @@ -56,7 +56,7 @@ public class ProjectAnalyzer
/// </remarks>
public IReadOnlyDictionary<string, string> EnvironmentVariables => GetEffectiveEnvironmentVariables(null);

public IEnumerable<ILogger> BuildLoggers => _loggers;
public IEnumerable<ILogger> BuildLoggers => _buildLoggers;

public ILogger<ProjectAnalyzer> Logger { get; set; }

Expand All @@ -65,7 +65,6 @@ public class ProjectAnalyzer
/// </summary>
public bool IgnoreFaultyImports { get; set; } = true;


// The project file path should already be normalized
internal ProjectAnalyzer(AnalyzerManager manager, string projectFilePath, ProjectInSolution projectInSolution)
{
Expand Down Expand Up @@ -235,7 +234,7 @@ private AnalyzerResults BuildTargets(BuildEnvironment buildEnvironment, string t
{
using (AnonymousPipeLoggerServer pipeLogger = new AnonymousPipeLoggerServer(cancellation.Token))
{
using (EventProcessor eventProcessor = new EventProcessor(this, BuildLoggers, pipeLogger, results != null))
using (EventProcessor eventProcessor = new EventProcessor(Manager, this, BuildLoggers, pipeLogger, results != null))
{
// Run MSBuild
int exitCode;
Expand Down Expand Up @@ -271,7 +270,7 @@ private string GetCommand(BuildEnvironment buildEnvironment, string targetFramew

// Get the logger arguments
string loggerPath = typeof(BuildalyzerLogger).Assembly.Location;
bool logEverything = _loggers.Count > 0;
bool logEverything = _buildLoggers.Count > 0;
string loggerArgument = $"/l:{nameof(BuildalyzerLogger)},{FormatArgument(loggerPath)};{pipeLoggerClientHandle};{logEverything}";

// Get the properties arguments
Expand Down Expand Up @@ -385,7 +384,7 @@ public void AddBuildLogger(ILogger logger)
throw new ArgumentNullException(nameof(logger));
}

_loggers.Add(logger);
_buildLoggers.Add(logger);
}

/// <summary>
Expand All @@ -399,7 +398,7 @@ public void RemoveBuildLogger(ILogger logger)
throw new ArgumentNullException(nameof(logger));
}

_loggers.Remove(logger);
_buildLoggers.Remove(logger);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public class OpenSourceProjectsFixture

private static TestRepository[] Repositories =
{
new TestRepository("https://github.com/autofac/Autofac.git"),
new TestRepository("https://github.com/autofac/Autofac.git",
@"\bench\Autofac.Benchmarks\Autofac.Benchmarks.csproj"),
new TestRepository("https://github.com/AutoMapper/AutoMapper.git"),
new TestRepository(EnvironmentPreference.Framework, "https://github.com/JamesNK/Newtonsoft.Json.git"), // Contains portable project, can't build using SDK
new TestRepository("https://github.com/nodatime/nodatime.git",
Expand Down
39 changes: 39 additions & 0 deletions tests/Buildalyzer.Tests/Integration/SimpleProjectsFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,45 @@ public void GetsReferences(
}
}

[Test]
public void GetsSourceFilesFromBinaryLog(
[ValueSource(nameof(Preferences))] EnvironmentPreference preference,
[ValueSource(nameof(ProjectFiles))] string projectFile)
{
// Given
StringWriter log = new StringWriter();
ProjectAnalyzer analyzer = GetProjectAnalyzer(projectFile, log);
EnvironmentOptions options = new EnvironmentOptions
{
Preference = preference
};
string binLogPath = Path.ChangeExtension(Path.GetTempFileName(), ".binlog");
analyzer.AddBinaryLogger(binLogPath);

try
{
// When
analyzer.Build(options);
IReadOnlyList<string> sourceFiles = analyzer.Manager.Analyze(binLogPath).First().SourceFiles;

// Then
sourceFiles.ShouldNotBeNull(log.ToString());
new[]
{
"Class1",
"AssemblyAttributes",
"AssemblyInfo"
}.ShouldBeSubsetOf(sourceFiles.Select(x => Path.GetFileName(x).Split('.').TakeLast(2).First()), log.ToString());
}
finally
{
if(File.Exists(binLogPath))
{
File.Delete(binLogPath);
}
}
}

#if Is_Windows
[Test]
public void MultiTargetingBuildAllTargetFrameworksGetsSourceFiles()
Expand Down

0 comments on commit c80ee21

Please sign in to comment.