ho modificato la mia classe Logger a darmi quello che voglio, ma sto ancora sperando per una soluzione più "nativa". Eccolo, nel caso in cui qualcun altro lo trovi utile.
L'idea generale è di notare il tempo di modifica del file prima della creazione del progetto e di annotarlo nuovamente in seguito. Se è cambiato, supponi che il progetto sia stato ricompilato.
Ho iniziato con il MSDN example, e modificato questi metodi:
eventSource_ProjectStarted
eventSource_ProjectFinished
Se si avvia lì, poi il resto dovrebbe essere abbastanza chiaro. Sono felice di rispondere alle domande se qualcuno le ha.
Ancora meglio, se qualcuno può venire a rispondere a questa risposta e dire "Perché non fai X", allora sarei molto felice di sapere che cos'è "X".
using System;
using System.Collections.Generic;
using System.IO;
using System.Security;
using BuildMan.Classes;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace JustBuild
{
public struct ProjectOutputTimeStamp
{
public string ProjectName;
public DateTime OutputDateTime_BeforeBuild;
public DateTime OutputDateTime_AfterBuild;
}
public class CRMBuildLogger : Logger
{
public List<string> Errors = new List<string>();
public List<string> Warnings = new List<string>();
public List<string> Messages = new List<string>();
public List<ProjectOutputTimeStamp> outputs = new List<ProjectOutputTimeStamp>();
public BuildResult BuildResult;
/// <summary>
/// Initialize is guaranteed to be called by MSBuild at the start of the build
/// before any events are raised.
/// </summary>
public override void Initialize(IEventSource eventSource)
{
if (null == Parameters)
{
throw new LoggerException("Log file was not set.");
}
string[] parameters = Parameters.Split(';');
string logFile = parameters[0];
if (String.IsNullOrEmpty(logFile))
{
throw new LoggerException("Log file was not set.");
}
if (parameters.Length > 1)
{
throw new LoggerException("Too many parameters passed.");
}
try
{
// Open the file
streamWriter = new StreamWriter(logFile);
}
catch (Exception ex)
{
if
(
ex is UnauthorizedAccessException
|| ex is ArgumentNullException
|| ex is PathTooLongException
|| ex is DirectoryNotFoundException
|| ex is NotSupportedException
|| ex is ArgumentException
|| ex is SecurityException
|| ex is IOException
)
{
throw new LoggerException("Failed to create log file: " + ex.Message);
}
// Unexpected failure
throw;
}
// For brevity, we'll only register for certain event types. Loggers can also
// register to handle TargetStarted/Finished and other events.
if (eventSource == null) return;
eventSource.ProjectStarted += eventSource_ProjectStarted;
eventSource.MessageRaised += eventSource_MessageRaised;
eventSource.WarningRaised += eventSource_WarningRaised;
eventSource.ErrorRaised += eventSource_ErrorRaised;
eventSource.ProjectFinished += eventSource_ProjectFinished;
}
void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
{
// BuildErrorEventArgs adds LineNumber, ColumnNumber, File, amongst other parameters
string line = String.Format(": ERROR {0}({1},{2}): ", e.File, e.LineNumber, e.ColumnNumber);
Errors.Add(line);
WriteLineWithSenderAndMessage(line, e);
}
void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
{
// BuildWarningEventArgs adds LineNumber, ColumnNumber, File, amongst other parameters
string line = String.Format(": Warning {0}({1},{2}): ", e.File, e.LineNumber, e.ColumnNumber);
Warnings.Add(line);
WriteLineWithSenderAndMessage(line, e);
}
void eventSource_MessageRaised(object sender, BuildMessageEventArgs e)
{
// BuildMessageEventArgs adds Importance to BuildEventArgs
// Let's take account of the verbosity setting we've been passed in deciding whether to log the message
if ((e.Importance == MessageImportance.High && IsVerbosityAtLeast(LoggerVerbosity.Minimal))
|| (e.Importance == MessageImportance.Normal && IsVerbosityAtLeast(LoggerVerbosity.Normal))
|| (e.Importance == MessageImportance.Low && IsVerbosityAtLeast(LoggerVerbosity.Detailed))
)
{
Messages.Add(e.Message);
WriteLineWithSenderAndMessage(String.Empty, e);
}
}
void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
{
int idx = IndexOfProjectTimeStamp(e.ProjectFile);
DateTime outputfiledatetime = DateTime.MinValue;
StudioProject proj = new StudioProject(e.ProjectFile);
FileInfo outputFile;
if (File.Exists(e.ProjectFile))
{
outputFile = new FileInfo(proj.OutputFile());
outputfiledatetime = outputFile.LastWriteTime;
}
//keep track of the mod date/time of the project output.
//if the mod date changes as a result of the build, then that means the project changed.
//this is necessary because the MSBuild engine doesn't tell us which projects were actually recompiled during a "build".
//see also: http://stackoverflow.com/questions/34903800
ProjectOutputTimeStamp p = new ProjectOutputTimeStamp()
{
OutputDateTime_BeforeBuild = outputfiledatetime,
ProjectName = e.ProjectFile,
OutputDateTime_AfterBuild = DateTime.MinValue
};
if (-1 == idx)
outputs.Add(p);
else
outputs[idx] = p;
WriteLine(String.Empty, e);
indent++;
}
private int IndexOfProjectTimeStamp(string projectname)
{
for (int i = 0; i < outputs.Count; ++i)
if (outputs[i].ProjectName.ToUpper() == projectname.ToUpper())
return i;
return -1;
}
void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
{
int idx = IndexOfProjectTimeStamp(e.ProjectFile);
DateTime outputfiledatetime = DateTime.MinValue;
StudioProject proj = new StudioProject(e.ProjectFile);
FileInfo outputFile;
if (File.Exists(e.ProjectFile))
{
outputFile = new FileInfo(proj.OutputFile());
outputfiledatetime = outputFile.LastWriteTime;
}
//keep track of the mod date/time of the project output.
//if the mod date changes as a result of the build, then that means the project changed.
//this is necessary because the MSBuild engine doesn't tell us which projects were actually recompiled during a "build".
//see also: http://stackoverflow.com/questions/34903800
ProjectOutputTimeStamp p = outputs[idx];
p.OutputDateTime_AfterBuild = outputfiledatetime;
if (-1 < idx)
outputs[idx] = p;
indent--;
WriteLine(String.Empty, e);
}
public List<string> RecompiledProjects()
{
//let callers ask "which projects were actually recompiled" and get a list of VBPROJ files.
List<string> result = new List<string>();
foreach (ProjectOutputTimeStamp p in outputs)
{
if(p.OutputDateTime_AfterBuild>p.OutputDateTime_BeforeBuild)
result.Add(p.ProjectName);
}
return result;
}
/// <summary>
/// Write a line to the log, adding the SenderName and Message
/// (these parameters are on all MSBuild event argument objects)
/// </summary>
private void WriteLineWithSenderAndMessage(string line, BuildEventArgs e)
{
if (0 == String.Compare(e.SenderName, "MSBuild", StringComparison.OrdinalIgnoreCase))
{
// Well, if the sender name is MSBuild, let's leave it out for prettiness
WriteLine(line, e);
}
else
{
WriteLine(e.SenderName + ": " + line, e);
}
}
/// <summary>
/// Just write a line to the log
/// </summary>
private void WriteLine(string line, BuildEventArgs e)
{
for (int i = indent; i > 0; i--)
{
streamWriter.Write("\t");
}
streamWriter.WriteLine(line + e.Message);
}
/// <summary>
/// Shutdown() is guaranteed to be called by MSBuild at the end of the build, after all
/// events have been raised.
/// </summary>
public override void Shutdown()
{
// Done logging, let go of the file
streamWriter.Close();
}
private StreamWriter streamWriter;
private int indent;
}
}
Si noti che la classe "StudioProject" è quella che ho scritto. Non voglio postare tutto perché ha un sacco di cose che fanno supposizioni che sarebbero vere solo nel nostro codice locale. Tuttavia, il metodo rilevante ("OutputFile") è qui. Fa una scansione piuttosto stupida attraverso il file di progetto stesso per capire l'output EXE o DLL.
public string OutputFile()
{
if (_ProjectFile == null) return string.Empty;
string result = string.Empty;
StreamReader reader = new StreamReader(_ProjectFile);
string projFolder = new DirectoryInfo(_ProjectFile).Parent?.FullName;
bool insideCurrentConfig = false;
string configuration = string.Empty;
string assemblyName = string.Empty;
string outputPath = string.Empty;
bool isExe = false;
do
{
string currentLine = reader.ReadLine();
if (currentLine == null) continue;
if ((configuration == string.Empty) && (currentLine.Contains("<Configuration"))) configuration = currentLine.Split('>')[1].Split('<')[0];
if (!insideCurrentConfig && !isExe && currentLine.Contains("WinExe")) isExe = true;
if ((assemblyName == string.Empty) && (currentLine.Contains("<AssemblyName>"))) assemblyName = currentLine.Split('>')[1].Split('<')[0];
if (configuration != string.Empty && currentLine.Contains("<PropertyGroup") && currentLine.Contains(configuration)) insideCurrentConfig = true;
if (insideCurrentConfig && currentLine.Contains("<OutputPath>")) outputPath = currentLine.Split('>')[1].Split('<')[0];
if ((outputPath != null) && (assemblyName != null)) result = projFolder + "\\" + outputPath + assemblyName + (isExe?".exe":".dll");
if (insideCurrentConfig && currentLine.Contains("</PropertyGroup>")) return result; //if we were in the current config, and that config is ending, then we are done.
} while (!reader.EndOfStream);
return result;
}
Forse si può provare a guardare qualcosa come https://github.com/nagits/BuildVision e vedere come è fatto lì. –
@OmarElabd Non avevo familiarità con questo progetto; ma lo sto verificando ora. Sembra interessante, indipendentemente dal fatto che sia d'aiuto con questo problema specifico. – JosephStyons
@OmarElabd che sta solo seguendo; Mi piace questo progetto e sono sicuro di poter imparare molto da esso. Tuttavia, non penso che mi possa aiutare con questo particolare problema. Sebbene ti consenta di avviare una ricostruzione o una ricostruzione, non vedo da nessuna parte che sembra a conoscenza del fatto che una build abbia effettivamente richiesto una ricompilazione. Continuerò a cercare nel caso in cui non l'ho ancora notato. – JosephStyons