diff --git a/src/Build.UnitTests/BinaryLogger_Tests.cs b/src/Build.UnitTests/BinaryLogger_Tests.cs index 7156a81eb58..55884569139 100644 --- a/src/Build.UnitTests/BinaryLogger_Tests.cs +++ b/src/Build.UnitTests/BinaryLogger_Tests.cs @@ -748,6 +748,182 @@ public void ParseParameters_InvalidParameter_ThrowsLoggerException(string parame File.Create(_logFile).Dispose(); } + #region ExtractFilePathFromParameters Tests + + [Theory] + [InlineData(null, "msbuild.binlog")] + [InlineData("", "msbuild.binlog")] + [InlineData("output.binlog", "output.binlog")] + [InlineData("LogFile=output.binlog", "output.binlog")] + [InlineData("output.binlog;ProjectImports=None", "output.binlog")] + [InlineData("ProjectImports=None;output.binlog", "output.binlog")] + [InlineData("ProjectImports=None;LogFile=output.binlog;OmitInitialInfo", "output.binlog")] + [InlineData("ProjectImports=None", "msbuild.binlog")] // No path specified + [InlineData("OmitInitialInfo", "msbuild.binlog")] // No path specified + public void ExtractFilePathFromParameters_ReturnsExpectedPath(string parameters, string expectedFileName) + { + string result = BinaryLogger.ExtractFilePathFromParameters(parameters); + + // The method returns full paths, so check just the filename + Path.GetFileName(result).ShouldBe(expectedFileName); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ExtractFilePathFromParameters_ReturnsFullPath() + { + string result = BinaryLogger.ExtractFilePathFromParameters("mylog.binlog"); + + // Should be a full path, not relative + Path.IsPathRooted(result).ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + #endregion + + #region ExtractNonPathParameters Tests + + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData("output.binlog", "")] // Path only, no config + [InlineData("LogFile=output.binlog", "")] // Path only, no config + [InlineData("ProjectImports=None", "ProjectImports=None")] + [InlineData("OmitInitialInfo", "OmitInitialInfo")] + [InlineData("output.binlog;ProjectImports=None", "ProjectImports=None")] + [InlineData("output.binlog;ProjectImports=None;OmitInitialInfo", "OmitInitialInfo;ProjectImports=None")] // Sorted + [InlineData("OmitInitialInfo;output.binlog;ProjectImports=None", "OmitInitialInfo;ProjectImports=None")] // Sorted + public void ExtractNonPathParameters_ReturnsExpectedConfig(string parameters, string expectedConfig) + { + string result = BinaryLogger.ExtractNonPathParameters(parameters); + result.ShouldBe(expectedConfig); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + #endregion + + #region ProcessParameters Tests + + [Fact] + public void ProcessParameters_NullArray_ReturnsEmptyResult() + { + var result = BinaryLogger.ProcessParameters(null); + + result.DistinctParameterSets.ShouldBeEmpty(); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + result.AllConfigurationsIdentical.ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_EmptyArray_ReturnsEmptyResult() + { + var result = BinaryLogger.ProcessParameters(Array.Empty()); + + result.DistinctParameterSets.ShouldBeEmpty(); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + result.AllConfigurationsIdentical.ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_SingleParameter_ReturnsOneParameterSet() + { + var result = BinaryLogger.ProcessParameters(new[] { "output.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(1); + result.DistinctParameterSets[0].ShouldBe("output.binlog"); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + result.AllConfigurationsIdentical.ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_MultipleIdenticalConfigs_OptimizesWithAdditionalPaths() + { + var result = BinaryLogger.ProcessParameters(new[] { "1.binlog", "2.binlog", "3.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(3); + result.AllConfigurationsIdentical.ShouldBeTrue(); + result.AdditionalFilePaths.Count.ShouldBe(2); // 2.binlog and 3.binlog + result.DuplicateFilePaths.ShouldBeEmpty(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_DifferentConfigs_NoOptimization() + { + var result = BinaryLogger.ProcessParameters(new[] { "1.binlog", "2.binlog;ProjectImports=None" }); + + result.DistinctParameterSets.Count.ShouldBe(2); + result.AllConfigurationsIdentical.ShouldBeFalse(); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_DuplicatePaths_FilteredOut() + { + var result = BinaryLogger.ProcessParameters(new[] { "1.binlog", "1.binlog", "2.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(2); // 1.binlog and 2.binlog + result.DuplicateFilePaths.Count.ShouldBe(1); // One duplicate of 1.binlog + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_DuplicatePaths_CaseInsensitive() + { + var result = BinaryLogger.ProcessParameters(new[] { "Output.binlog", "output.BINLOG", "other.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(2); // Output.binlog and other.binlog + result.DuplicateFilePaths.Count.ShouldBe(1); // One duplicate + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_MixedConfigsWithDuplicates_HandledCorrectly() + { + var result = BinaryLogger.ProcessParameters(new[] { + "1.binlog", + "2.binlog;ProjectImports=None", + "1.binlog;ProjectImports=None" // Different config but same path - filtered as duplicate + }); + + result.DistinctParameterSets.Count.ShouldBe(2); + result.AllConfigurationsIdentical.ShouldBeFalse(); + result.DuplicateFilePaths.Count.ShouldBe(1); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + #endregion + public void Dispose() { _env.Dispose(); diff --git a/src/Build/Logging/BinaryLogger/BinaryLogger.cs b/src/Build/Logging/BinaryLogger/BinaryLogger.cs index 3d40ddeb7af..73031a02798 100644 --- a/src/Build/Logging/BinaryLogger/BinaryLogger.cs +++ b/src/Build/Logging/BinaryLogger/BinaryLogger.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; @@ -296,6 +297,17 @@ private static bool TryParsePathParameter(string parameter, out string filePath) internal string FilePath { get; private set; } + /// + /// Gets or sets additional output file paths. When set, the binlog will be copied to all these paths + /// after the build completes. The primary FilePath will be used as the temporary write location. + /// + /// + /// This property is intended for internal use by MSBuild command-line processing. + /// It should not be set by external code or logger implementations. + /// Use multiple logger instances with different Parameters instead. + /// + public IReadOnlyList AdditionalFilePaths { get; init; } + /// Gets or sets the verbosity level. /// /// The binary logger Verbosity is always maximum (Diagnostic). It tries to capture as much @@ -505,6 +517,15 @@ public void Shutdown() } + // Log additional file paths before closing stream (so they're recorded in the binlog) + if (AdditionalFilePaths != null && AdditionalFilePaths.Count > 0 && stream != null) + { + foreach (var additionalPath in AdditionalFilePaths) + { + LogMessage("BinLogCopyDestination=" + additionalPath); + } + } + if (stream != null) { // It's hard to determine whether we're at the end of decoding GZipStream @@ -514,6 +535,37 @@ public void Shutdown() stream.Dispose(); stream = null; } + + // Copy the binlog file to additional destinations if specified + if (AdditionalFilePaths != null && AdditionalFilePaths.Count > 0) + { + foreach (var additionalPath in AdditionalFilePaths) + { + try + { + string directory = Path.GetDirectoryName(additionalPath); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + File.Copy(FilePath, additionalPath, overwrite: true); + } + catch (Exception ex) + { + // Log the error but don't fail the build + // Note: We can't use LogMessage here since the stream is already closed + string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword( + out _, + out _, + "ErrorCopyingBinaryLog", + FilePath, + additionalPath, + ex.Message); + + Console.Error.WriteLine(message); + } + } + } } private void RawEvents_LogDataSliceReceived(BinaryLogRecordKind recordKind, Stream stream) @@ -635,6 +687,78 @@ private void ProcessParameters(out bool omitInitialInfo) } private bool TryInterpretPathParameter(string parameter, out string filePath) + { + return TryInterpretPathParameterCore(parameter, GetUniqueStamp, out filePath); + } + + private string GetUniqueStamp() + => (PathParameterExpander ?? ExpandPathParameter)(string.Empty); + + private static string ExpandPathParameter(string parameters) + => $"{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss")}--{EnvironmentUtilities.CurrentProcessId}--{StringUtils.GenerateRandomString(6)}"; + + /// + /// Extracts the file path from binary logger parameters string. + /// This is a helper method for processing multiple binlog parameters. + /// + /// The parameters string (e.g., "output.binlog" or "output.binlog;ProjectImports=None") + /// The resolved file path, or "msbuild.binlog" if no path is specified + public static string ExtractFilePathFromParameters(string parameters) + { + const string DefaultBinlogFileName = "msbuild" + BinlogFileExtension; + + if (string.IsNullOrEmpty(parameters)) + { + return Path.GetFullPath(DefaultBinlogFileName); + } + + var paramParts = parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries); + string filePath = null; + + foreach (var parameter in paramParts) + { + if (TryInterpretPathParameterStatic(parameter, out string extractedPath)) + { + filePath = extractedPath; + break; + } + } + + if (filePath == null) + { + filePath = DefaultBinlogFileName; + } + + try + { + return Path.GetFullPath(filePath); + } + catch + { + // If path resolution fails, return the original path + return filePath; + } + } + + /// + /// Attempts to interpret a parameter string as a file path. + /// + /// The parameter to interpret (e.g., "LogFile=output.binlog" or "output.binlog") + /// The extracted file path if the parameter is a path, otherwise the original parameter + /// True if the parameter is a valid file path (ends with .binlog or contains wildcards), false otherwise + private static bool TryInterpretPathParameterStatic(string parameter, out string filePath) + { + return TryInterpretPathParameterCore(parameter, () => ExpandPathParameter(string.Empty), out filePath); + } + + /// + /// Core logic for interpreting a parameter string as a file path. + /// + /// The parameter to interpret + /// Function to expand wildcard placeholders + /// The extracted file path + /// True if the parameter is a valid file path + private static bool TryInterpretPathParameterCore(string parameter, Func wildcardExpander, out string filePath) { bool hasPathPrefix = parameter.StartsWith(LogFileParameterPrefix, StringComparison.OrdinalIgnoreCase); @@ -654,7 +778,7 @@ private bool TryInterpretPathParameter(string parameter, out string filePath) return hasProperExtension; } - filePath = parameter.Replace("{}", GetUniqueStamp(), StringComparison.Ordinal); + filePath = parameter.Replace("{}", wildcardExpander(), StringComparison.Ordinal); if (!hasProperExtension) { @@ -663,10 +787,149 @@ private bool TryInterpretPathParameter(string parameter, out string filePath) return true; } - private string GetUniqueStamp() - => (PathParameterExpander ?? ExpandPathParameter)(string.Empty); + /// + /// Extracts the non-file-path parameters from binary logger parameters string. + /// This is used to compare configurations between multiple binlog parameters. + /// + /// The parameters string (e.g., "output.binlog;ProjectImports=None") + /// A normalized string of non-path parameters, or empty string if only path parameters + public static string ExtractNonPathParameters(string parameters) + { + if (string.IsNullOrEmpty(parameters)) + { + return string.Empty; + } - private static string ExpandPathParameter(string parameters) - => $"{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss")}--{EnvironmentUtilities.CurrentProcessId}--{StringUtils.GenerateRandomString(6)}"; + var paramParts = parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries); + var nonPathParams = new List(); + + foreach (var parameter in paramParts) + { + // Skip file path parameters + if (TryInterpretPathParameterStatic(parameter, out _)) + { + continue; + } + + // This is a configuration parameter (like ProjectImports=None, OmitInitialInfo, etc.) + nonPathParams.Add(parameter); + } + + // Sort for consistent comparison + nonPathParams.Sort(StringComparer.OrdinalIgnoreCase); + return string.Join(";", nonPathParams); + } + + /// + /// Result of processing multiple binary logger parameter sets. + /// + public readonly struct ProcessedBinaryLoggerParameters + { + /// + /// List of distinct parameter sets that need separate logger instances. + /// + public IReadOnlyList DistinctParameterSets { get; } + + /// + /// If true, all parameter sets have identical configurations (only file paths differ), + /// so a single logger can be used with file copying for additional paths. + /// + public bool AllConfigurationsIdentical { get; } + + /// + /// Additional file paths to copy the binlog to (only valid when AllConfigurationsIdentical is true). + /// + public IReadOnlyList AdditionalFilePaths { get; } + + /// + /// List of duplicate file paths that were filtered out. + /// + public IReadOnlyList DuplicateFilePaths { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// List of distinct parameter sets that need separate logger instances. + /// Whether all parameter sets have identical configurations. + /// Additional file paths to copy the binlog to. + /// List of duplicate file paths that were filtered out. + public ProcessedBinaryLoggerParameters( + IReadOnlyList distinctParameterSets, + bool allConfigurationsIdentical, + IReadOnlyList additionalFilePaths, + IReadOnlyList duplicateFilePaths) + { + DistinctParameterSets = distinctParameterSets; + AllConfigurationsIdentical = allConfigurationsIdentical; + AdditionalFilePaths = additionalFilePaths; + DuplicateFilePaths = duplicateFilePaths; + } + } + + /// + /// Processes multiple binary logger parameter sets and returns distinct paths and configuration info. + /// + /// Array of parameter strings from command line + /// Processed result with distinct parameter sets and configuration info + public static ProcessedBinaryLoggerParameters ProcessParameters(string[] binaryLoggerParameters) + { + var distinctParameterSets = new List(); + var additionalFilePaths = new List(); + var duplicateFilePaths = new List(); + bool allConfigurationsIdentical = true; + + if (binaryLoggerParameters == null || binaryLoggerParameters.Length == 0) + { + return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths); + } + + if (binaryLoggerParameters.Length == 1) + { + distinctParameterSets.Add(binaryLoggerParameters[0]); + return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths); + } + + string primaryArguments = binaryLoggerParameters[0]; + string primaryNonPathParams = ExtractNonPathParameters(primaryArguments); + + var distinctFilePaths = new HashSet(StringComparer.OrdinalIgnoreCase); + var extractedFilePaths = new List(); + + // Check if all parameter sets have the same non-path configuration + for (int i = 0; i < binaryLoggerParameters.Length; i++) + { + string currentParams = binaryLoggerParameters[i]; + string currentNonPathParams = ExtractNonPathParameters(currentParams); + string currentFilePath = ExtractFilePathFromParameters(currentParams); + + // Check if this is a duplicate file path + if (distinctFilePaths.Add(currentFilePath)) + { + if (!string.Equals(primaryNonPathParams, currentNonPathParams, StringComparison.OrdinalIgnoreCase)) + { + allConfigurationsIdentical = false; + } + distinctParameterSets.Add(currentParams); + extractedFilePaths.Add(currentFilePath); + } + else + { + // Track duplicate paths for logging + duplicateFilePaths.Add(currentFilePath); + } + } + + // If all configurations are identical, compute additional file paths for copying + // Use the pre-extracted paths to avoid redundant ExtractFilePathFromParameters calls + if (allConfigurationsIdentical && distinctParameterSets.Count > 1) + { + for (int i = 1; i < extractedFilePaths.Count; i++) + { + additionalFilePaths.Add(extractedFilePaths[i]); + } + } + + return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths); + } } } diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 68b59de6ee8..ad1c58e297f 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -2449,6 +2449,10 @@ Utilization: {0} Average Utilization: {1:###.0} The directory does not exist: {0}. .NET Runtime Task Host could not be instantiated. See https://aka.ms/nettaskhost for details on how to resolve this error. + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + The custom task '{0}' required a fallback to out-of-process execution because the UsingTask definition does not specify the correct Runtime and Architecture. This reduces build performance. Update the UsingTask element to explicitly specify Runtime and Architecture attributes (e.g., Runtime="CLR4" Architecture="x64") or use TaskFactory="TaskHostFactory". @@ -2459,7 +2463,7 @@ Utilization: {0} Average Utilization: {1:###.0} diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 86d6fc9c60c..bdc1325178e 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -489,6 +489,11 @@ Číst proměnnou prostředí {0} + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Při čtení vstupních souborů mezipaměti pro výsledky z cesty {0} byla zjištěna chyba: {1} diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index 4adaf0b3973..862813a6e21 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -489,6 +489,11 @@ Umgebungsvariable "{0}" lesen + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Beim Lesen der Cachedateien für Eingabeergebnisse aus dem Pfad "{0}" wurde ein Fehler festgestellt: {1} diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index ed841430d86..23917c6c2b6 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -489,6 +489,11 @@ Leer la variable de entorno "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Error al leer los archivos de caché de resultados de entrada en la ruta de acceso "{0}": {1} diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 2a866b00d4e..d5ff75804fe 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -489,6 +489,11 @@ Lire la variable d'environnement "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: La lecture des fichiers cache des résultats d'entrée à partir du chemin "{0}" a rencontré une erreur : {1} diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index 99d4affd579..e2d9e251798 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -489,6 +489,11 @@ Legge la variabile di ambiente "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: durante la lettura dei file della cache dei risultati di input dal percorso "{0}" è stato rilevato un errore: {1} diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index 38b60d181d3..0ad0043c745 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -489,6 +489,11 @@ 環境変数 "{0}" の読み取り + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: パス "{0}" から入力結果キャッシュ ファイルを読み取る処理でエラーが発生しました: {1} diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index e5aa1a02533..7f109b368dc 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -489,6 +489,11 @@ 환경 변수 "{0}" 읽기 + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: "{0}" 경로에서 입력 결과 캐시 파일을 읽는 중 오류가 발생했습니다. {1} diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index 866d90bcd9d..c0306633a55 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -489,6 +489,11 @@ Odczytaj zmienną środowiskową „{0}” + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Podczas odczytywania plików wejściowej pamięci podręcznej wyników ze ścieżki „{0}” wystąpił błąd: {1} diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index d27c50e5464..a4b99f65cd0 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -489,6 +489,11 @@ Ler a variável de ambiente "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: a leitura dos arquivos de cache do resultado de entrada do caminho "{0}" encontrou um erro: {1} diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 55d613e49cd..fd58805b3af 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -489,6 +489,11 @@ Чтение переменной среды "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: произошла ошибка при чтении входных файлов кэша результатов из пути "{0}": {1} diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index 7a5e481602b..c44ef91e643 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -489,6 +489,11 @@ "{0}" ortam değişkenini oku + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: "{0}" yolundan giriş sonucu önbellek dosyaları okunurken bir hatayla karşılaşıldı: {1} diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index d8da78803bc..addeadae2f8 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -489,6 +489,11 @@ 读取环境变量“{0}” + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: 从路径“{0}”读取输入结果缓存文件时遇到错误: {1} diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index d851b21ba23..fc8323d35b1 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -489,6 +489,11 @@ 讀取環境變數 "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: 從路徑 "{0}" 讀取輸入結果快取檔案發生錯誤: {1} diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index 4173c747739..51208f7a60f 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -2682,6 +2682,116 @@ public void BinaryLogContainsImportedFiles() archive.Entries.ShouldContain(e => e.FullName.EndsWith(".proj", StringComparison.OrdinalIgnoreCase), 2); } + [Fact] + public void MultipleBinaryLogsCreatesMultipleFiles() + { + var testProject = _env.CreateFile("TestProject.proj", @" + + + + + + "); + + string binLogLocation = _env.DefaultTestDirectory.Path; + string binLog1 = Path.Combine(binLogLocation, "1.binlog"); + string binLog2 = Path.Combine(binLogLocation, "2.binlog"); + string binLog3 = Path.Combine(binLogLocation, "3.binlog"); + + string output = RunnerUtilities.ExecMSBuild($"\"{testProject.Path}\" \"/bl:{binLog1}\" \"/bl:{binLog2}\" \"/bl:{binLog3}\"", out var success, _output); + + success.ShouldBeTrue(output); + + // Verify all three binlog files exist + File.Exists(binLog1).ShouldBeTrue("First binlog file should exist"); + File.Exists(binLog2).ShouldBeTrue("Second binlog file should exist"); + File.Exists(binLog3).ShouldBeTrue("Third binlog file should exist"); + + // Verify all files have content (are not empty) + new FileInfo(binLog1).Length.ShouldBeGreaterThan(0, "First binlog should not be empty"); + new FileInfo(binLog2).Length.ShouldBeGreaterThan(0, "Second binlog should not be empty"); + new FileInfo(binLog3).Length.ShouldBeGreaterThan(0, "Third binlog should not be empty"); + + // Verify all files are identical (have the same content) + byte[] file1Bytes = File.ReadAllBytes(binLog1); + byte[] file2Bytes = File.ReadAllBytes(binLog2); + byte[] file3Bytes = File.ReadAllBytes(binLog3); + + file1Bytes.SequenceEqual(file2Bytes).ShouldBeTrue("First and second binlog should be identical"); + file1Bytes.SequenceEqual(file3Bytes).ShouldBeTrue("First and third binlog should be identical"); + } + + [Fact] + public void MultipleBinaryLogsWithDuplicatesCreateDistinctFiles() + { + var testProject = _env.CreateFile("TestProject.proj", @" + + + + + + "); + + string binLogLocation = _env.DefaultTestDirectory.Path; + string binLog1 = Path.Combine(binLogLocation, "1.binlog"); + string binLog2 = Path.Combine(binLogLocation, "2.binlog"); + + // Specify binLog1 twice - should only create two distinct files + string output = RunnerUtilities.ExecMSBuild($"\"{testProject.Path}\" \"/bl:{binLog1}\" \"/bl:{binLog2}\" \"/bl:{binLog1}\"", out var success, _output); + + success.ShouldBeTrue(output); + + // Verify both binlog files exist + File.Exists(binLog1).ShouldBeTrue("First binlog file should exist"); + File.Exists(binLog2).ShouldBeTrue("Second binlog file should exist"); + + // Verify both files are identical + byte[] file1Bytes = File.ReadAllBytes(binLog1); + byte[] file2Bytes = File.ReadAllBytes(binLog2); + + file1Bytes.SequenceEqual(file2Bytes).ShouldBeTrue("Binlog files should be identical"); + } + + [Fact] + public void MultipleBinaryLogsWithDifferentConfigurationsCreatesSeparateLoggers() + { + var testProject = _env.CreateFile("TestProject.proj", @" + + + + + + + "); + + _env.CreateFile("Imported.proj", @" + + + Value + + + "); + + string binLogLocation = _env.DefaultTestDirectory.Path; + string binLog1 = Path.Combine(binLogLocation, "with-imports.binlog"); + string binLog2 = Path.Combine(binLogLocation, "no-imports.binlog"); + + // One with default imports, one with ProjectImports=None + string output = RunnerUtilities.ExecMSBuild($"\"{testProject.Path}\" \"/bl:{binLog1}\" \"/bl:{binLog2};ProjectImports=None\"", out var success, _output); + + success.ShouldBeTrue(output); + + // Verify both binlog files exist + File.Exists(binLog1).ShouldBeTrue("First binlog file should exist"); + File.Exists(binLog2).ShouldBeTrue("Second binlog file should exist"); + + // Verify files are different sizes (one has imports embedded, one doesn't) + long size1 = new FileInfo(binLog1).Length; + long size2 = new FileInfo(binLog2).Length; + + size1.ShouldBeGreaterThan(size2, "Binlog with imports should be larger than one without"); + } + [Theory] [InlineData("-warnaserror", "", "", false)] [InlineData("-warnaserror -warnnotaserror:FOR123", "", "", true)] diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index bf1e7bc6020..7fdf9799eb5 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1700,6 +1700,10 @@ 0: turned off + + Duplicate binary log path(s) specified and ignored: {0} + {0} is the list of duplicate paths that were filtered out +