From 0cb855adf9945615d60234dd6870e183ef910346 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Thu, 22 Feb 2024 21:26:28 +0100 Subject: [PATCH] Fix "original.dll" file permission errors during build (#13840) * Avoid constantly recompiling Avalonia Xaml files by implementing incremental build checks - Should be a (or even the) fix for file permission errors during builds CompileAvaloniaXaml no longer overwrites the Compile output, but creates its own output files - This supports incremental build checks and is safer in general Removed unused executable features from Avalonia.Build.Tasks - This is instead of refactoring for the new ITaskItem properties Updated Desktop SLNF * Fixed tests * Touch each copied output file --- Avalonia.Desktop.slnf | 5 +- packages/Avalonia/AvaloniaBuildTasks.targets | 79 ++++++++---- .../Avalonia.Build.Tasks.csproj | 1 - .../CompileAvaloniaXamlTask.cs | 116 ++++++------------ src/Avalonia.Build.Tasks/Program.cs | 86 ------------- .../Avalonia.Build.Tasks.UnitTest.csproj | 1 + .../CompileAvaloniaXamlTaskTest.cs | 17 ++- 7 files changed, 104 insertions(+), 201 deletions(-) delete mode 100644 src/Avalonia.Build.Tasks/Program.cs diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index c63d55f2b4..794cdf5d3b 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -51,6 +51,7 @@ "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", + "tests\\Avalonia.Build.Tasks.UnitTest\\Avalonia.Build.Tasks.UnitTest.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", "tests\\Avalonia.Controls.ItemsRepeater.UnitTests\\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "tests\\Avalonia.Controls.UnitTests\\Avalonia.Controls.UnitTests.csproj", @@ -58,6 +59,7 @@ "tests\\Avalonia.DesignerSupport.Tests\\Avalonia.DesignerSupport.Tests.csproj", "tests\\Avalonia.Direct2D1.RenderTests\\Avalonia.Direct2D1.RenderTests.csproj", "tests\\Avalonia.Direct2D1.UnitTests\\Avalonia.Direct2D1.UnitTests.csproj", + "tests\\Avalonia.Generators.Tests\\Avalonia.Generators.Tests.csproj", "tests\\Avalonia.IntegrationTests.Appium\\Avalonia.IntegrationTests.Appium.csproj", "tests\\Avalonia.LeakTests\\Avalonia.LeakTests.csproj", "tests\\Avalonia.Markup.UnitTests\\Avalonia.Markup.UnitTests.csproj", @@ -65,7 +67,8 @@ "tests\\Avalonia.ReactiveUI.UnitTests\\Avalonia.ReactiveUI.UnitTests.csproj", "tests\\Avalonia.Skia.RenderTests\\Avalonia.Skia.RenderTests.csproj", "tests\\Avalonia.Skia.UnitTests\\Avalonia.Skia.UnitTests.csproj", - "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" + "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj", + "tests\\TestFiles\\BuildTasks\\PInvoke\\PInvoke.csproj" ] } } \ No newline at end of file diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index ef5658f1ca..eef6d51cae 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -58,7 +58,7 @@ $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache;_GenerateNoWarnForExec - $(CompileAvaloniaXamlDependsOn);_GenerateNoWarnForExec + $(CompileAvaloniaXamlDependsOn);PrepareToCompileAvaloniaXaml;_GenerateNoWarnForExec @@ -114,38 +114,44 @@ Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:NoWarn=$(_NoWarnForExec) /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:BuildProjectReferences=false"/> - + $(IntermediateOutputPath)/Avalonia/references - $(IntermediateOutputPath)/Avalonia/original.dll false false false <_AvaloniaHasCompiledXaml>true - - + + + + <_DebugSymbolsIntermediatePath Update="*" AvaloniaCompileOutput="%(RelativeDir)Avalonia\%(Filename)%(Extension)"/> + + + + + + + + + + + + + + - - + AnalyzerConfigFiles="@(EditorConfigFiles)"/> + + + + + + + <_AvaloniaXamlCompiledAssembly Include="@(IntermediateAssembly->Metadata('AvaloniaCompileOutput'))"/> + + + + <_AvaloniaXamlCompiledRefAssembly Include="@(IntermediateRefAssembly->Metadata('AvaloniaCompileOutput'))"/> + + + <_AvaloniaXamlCompiledSymbols Include="@(_DebugSymbolsIntermediatePath->Metadata('AvaloniaCompileOutput'))"/> + <_DebugSymbolsIntermediatePath Remove="@(_DebugSymbolsIntermediatePath)"/> + <_DebugSymbolsIntermediatePath Include="@(_AvaloniaXamlCompiledSymbols)"/> + + + + + + + + diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 3d83ca6a50..49b9e7cddc 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -1,7 +1,6 @@  netstandard2.0 - exe false tools $(DefineConstants);BUILDTASK;XAMLX_CECIL_INTERNAL;XAMLX_INTERNAL diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index 5287827652..8cd444d30b 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Build.Framework; @@ -8,110 +7,70 @@ namespace Avalonia.Build.Tasks { public class CompileAvaloniaXamlTask: ITask { + public const string AvaloniaCompileOutputMetadataName = "AvaloniaCompileOutput"; + public bool Execute() { Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance); - var writtenFilePaths = new List(); - - OutputPath ??= AssemblyFile; - RefOutputPath ??= RefAssemblyFile; - var outputPdb = GetPdbPath(OutputPath); - var input = AssemblyFile; - var refInput = RefOutputPath; - var inputPdb = GetPdbPath(input); - // Make a copy and delete the original file to prevent MSBuild from thinking that everything is OK - if (OriginalCopyPath != null) - { - var originalCopyPathRef = Path.ChangeExtension(OriginalCopyPath, ".ref.dll"); - File.Copy(AssemblyFile, OriginalCopyPath, true); - writtenFilePaths.Add(OriginalCopyPath); - input = OriginalCopyPath; - File.Delete(AssemblyFile); + var outputPath = AssemblyFile.GetMetadata(AvaloniaCompileOutputMetadataName); + var refOutputPath = RefAssemblyFile?.GetMetadata(AvaloniaCompileOutputMetadataName); - if (File.Exists(inputPdb)) - { - var copyPdb = GetPdbPath(OriginalCopyPath); - File.Copy(inputPdb, copyPdb, true); - writtenFilePaths.Add(copyPdb); - File.Delete(inputPdb); - inputPdb = copyPdb; - } - - if (!string.IsNullOrWhiteSpace(RefAssemblyFile) && File.Exists(RefAssemblyFile)) - { - // We also copy ref assembly just for case if needed later for testing. - // But do not remove the original one, as MSBuild actually complains about it with multi-thread compiling. - File.Copy(RefAssemblyFile, originalCopyPathRef, true); - writtenFilePaths.Add(originalCopyPathRef); - refInput = originalCopyPathRef; - } + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + if (!string.IsNullOrEmpty(refOutputPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(refOutputPath)); } - var msg = $"CompileAvaloniaXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}"; + var msg = $"CompileAvaloniaXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{outputPath}"; BuildEngine.LogMessage(msg, outputImportance < MessageImportance.Low ? MessageImportance.High : outputImportance); var res = XamlCompilerTaskExecutor.Compile(BuildEngine, - input, OutputPath, - refInput, RefOutputPath, - File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), + AssemblyFile.ItemSpec, outputPath, + RefAssemblyFile?.ItemSpec, refOutputPath, + References?.Select(i => i.ItemSpec).ToArray() ?? Array.Empty(), ProjectDirectory, VerifyIl, DefaultCompileBindings, outputImportance, new XamlCompilerDiagnosticsFilter(AnalyzerConfigFiles), (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, SkipXamlCompilation, DebuggerLaunch, VerboseExceptions); - if (!res.Success) - { - WrittenFilePaths = writtenFilePaths.ToArray(); - return false; - } - if (!res.WrittenFile) + if (res.Success && !res.WrittenFile) { - File.Copy(input, OutputPath, true); - if (File.Exists(inputPdb)) - File.Copy(inputPdb, outputPdb, true); - } - else if (!string.IsNullOrWhiteSpace(RefOutputPath) && File.Exists(RefOutputPath)) - writtenFilePaths.Add(RefOutputPath); + // To simplify incremental build checks, copy the input files to the expected output locations even if the Xaml compiler didn't do anything. + CopyAndTouch(AssemblyFile.ItemSpec, outputPath); + CopyAndTouch(Path.ChangeExtension(AssemblyFile.ItemSpec, ".pdb"), Path.ChangeExtension(outputPath, ".pdb")); - writtenFilePaths.Add(OutputPath); - if (File.Exists(outputPdb)) - writtenFilePaths.Add(outputPdb); + if (!string.IsNullOrEmpty(refOutputPath)) + { + CopyAndTouch(RefAssemblyFile.ItemSpec, refOutputPath); + } + } - WrittenFilePaths = writtenFilePaths.ToArray(); - return true; + return res.Success; } - string GetPdbPath(string p) + private static void CopyAndTouch(string source, string destination) { - var d = Path.GetDirectoryName(p); - var f = Path.GetFileNameWithoutExtension(p); - var rv = f + ".pdb"; - if (d != null) - rv = Path.Combine(d, rv); - return rv; + File.Copy(source, destination, overwrite: true); + File.SetLastWriteTimeUtc(destination, DateTime.UtcNow); } - - [Required] - public string AssemblyFile { get; set; } - [Required] - public string ReferencesFilePath { get; set; } - [Required] - public string OriginalCopyPath { get; set; } + [Required] public string ProjectDirectory { get; set; } - - public string RefAssemblyFile { get; set; } - public string RefOutputPath { get; set; } - - public string OutputPath { get; set; } + + [Required] + public ITaskItem AssemblyFile { get; set; } + + public ITaskItem? RefAssemblyFile { get; set; } + + public ITaskItem[]? References { get; set; } public bool VerifyIl { get; set; } public bool DefaultCompileBindings { get; set; } - + public bool SkipXamlCompilation { get; set; } - + public string AssemblyOriginatorKeyFile { get; set; } public bool SignAssembly { get; set; } public bool DelaySign { get; set; } @@ -124,10 +83,7 @@ namespace Avalonia.Build.Tasks public bool DebuggerLaunch { get; set; } public bool VerboseExceptions { get; set; } - + public ITaskItem[] AnalyzerConfigFiles { get; set; } - - [Output] - public string[] WrittenFilePaths { get; private set; } = Array.Empty(); } } diff --git a/src/Avalonia.Build.Tasks/Program.cs b/src/Avalonia.Build.Tasks/Program.cs deleted file mode 100644 index 8ddea2d142..0000000000 --- a/src/Avalonia.Build.Tasks/Program.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections; -using System.IO; -using System.Linq; -using Microsoft.Build.Framework; - -namespace Avalonia.Build.Tasks -{ - public class Program - { - private const string OriginalDll = "original.dll"; - private const string References = "references"; - private const string OutDll = "out.dll"; - - static int Main(string[] args) - { - if (args.Length < 3) - { - if (args.Length == 1) - { - args = new[] {OriginalDll, References, OutDll} - .Select(x => Path.Combine(args[0], x)).ToArray(); - } - else - { - const string referencesOutputPath = "path/to/Avalonia/samples/Sandbox/obj/Debug/net60/Avalonia"; - Console.WriteLine(@$"Usage: - 1) dotnet ./Avalonia.Build.Tasks.dll - , where likes {referencesOutputPath} - 2) dotnet ./Avalonia.Build.Tasks.dll - , where: - - likes {referencesOutputPath}/{OriginalDll} - - likes {referencesOutputPath}/{References} - - likes {referencesOutputPath}/{OutDll} - - Likes {referencesOutputPath}/original.ref.dll"); - - return 1; - } - } - - return new CompileAvaloniaXamlTask() - { - AssemblyFile = args[0], - ReferencesFilePath = args[1], - OutputPath = args[2], - RefAssemblyFile = args.Length > 3 ? args[3] : null, - BuildEngine = new ConsoleBuildEngine(), - ProjectDirectory = Directory.GetCurrentDirectory(), - VerifyIl = true - }.Execute() ? - 0 : - 2; - } - - class ConsoleBuildEngine : IBuildEngine - { - public void LogErrorEvent(BuildErrorEventArgs e) - { - Console.WriteLine($"ERROR: {e.Code} {e.Message} in {e.File} {e.LineNumber}:{e.ColumnNumber}-{e.EndLineNumber}:{e.EndColumnNumber}"); - } - - public void LogWarningEvent(BuildWarningEventArgs e) - { - Console.WriteLine($"WARNING: {e.Code} {e.Message} in {e.File} {e.LineNumber}:{e.ColumnNumber}-{e.EndLineNumber}:{e.EndColumnNumber}"); - } - - public void LogMessageEvent(BuildMessageEventArgs e) - { - Console.WriteLine($"MESSAGE: {e.Code} {e.Message} in {e.File} {e.LineNumber}:{e.ColumnNumber}-{e.EndLineNumber}:{e.EndColumnNumber}"); - } - - public void LogCustomEvent(CustomBuildEventArgs e) - { - Console.WriteLine($"CUSTOM: {e.Message}"); - } - - public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, - IDictionary targetOutputs) => throw new NotSupportedException(); - - public bool ContinueOnError { get; } - public int LineNumberOfTaskNode { get; } - public int ColumnNumberOfTaskNode { get; } - public string ProjectFileOfTaskNode { get; } - } - } -} diff --git a/tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj b/tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj index 0a289ff28b..7182b0d0f7 100644 --- a/tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj +++ b/tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj @@ -25,6 +25,7 @@ + diff --git a/tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs b/tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs index fee678c388..115457926b 100644 --- a/tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs +++ b/tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs @@ -1,6 +1,8 @@ using System; using System.IO; +using System.Linq; using System.Reflection; +using Microsoft.Build.Utilities; using Xunit; namespace Avalonia.Build.Tasks.UnitTest; @@ -13,19 +15,16 @@ public class CompileAvaloniaXamlTaskTest { using var engine = UnitTestBuildEngine.Start(); var basePath = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath), "Assets"); - var originalAssemblyPath = Path.Combine(basePath, - "PInvoke.dll"); - var referencesPath = Path.Combine(basePath, - "PInvoke.dll.refs"); - var compiledAssemblyPath = "PInvoke.dll"; + var assembly = new TaskItem(Path.Combine(basePath, "PInvoke.dll")); + assembly.SetMetadata(CompileAvaloniaXamlTask.AvaloniaCompileOutputMetadataName, Path.Combine(basePath, "Avalonia", Path.GetFileName(assembly.ItemSpec))); + var references = File.ReadAllLines(Path.Combine(basePath, "PInvoke.dll.refs")).Select(p => new TaskItem(p)).ToArray(); - Assert.True(File.Exists(originalAssemblyPath), $"The original {originalAssemblyPath} don't exists."); + Assert.True(File.Exists(assembly.ItemSpec), $"The original {assembly.ItemSpec} don't exist."); new CompileAvaloniaXamlTask() { - AssemblyFile = originalAssemblyPath, - ReferencesFilePath = referencesPath, - OutputPath = compiledAssemblyPath, + AssemblyFile = assembly, + References = references, RefAssemblyFile = null, BuildEngine = engine, ProjectDirectory = Directory.GetCurrentDirectory(),