Skip to content

Commit bed48a7

Browse files
authored
Rework XAML compilation targets (#17539)
* Added BuildTests projects * Added VerifyXamlCompilation build target * Use TargetsTriggerByCompilation for XAML compilation * Add *.binlog to gitignore * VerifyXamlCompilation target: set NuGetPackageRoot * Ensure WpfHybrid build test uses two markup compilation passes * Fail build tests restore immediately if AvaloniaVersion isn't set * Fix "could not extract MVID" for up-to-date builds * Run VerifyXamlCompilation on CI * Add FSharp build test
1 parent c851c7a commit bed48a7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1017
-79
lines changed

Diff for: .gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ x64/
4545
*.vssscc
4646
.builds
4747
*.pidb
48-
*.log
4948
*.scc
49+
*.binlog
5050

5151
# Visual C++ cache files
5252
ipch/

Diff for: .nuke/build.schema.json

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"RunTests",
9595
"RunToolsTests",
9696
"ValidateApiDiff",
97+
"VerifyXamlCompilation",
9798
"ZipFiles"
9899
]
99100
}
@@ -131,6 +132,7 @@
131132
"RunTests",
132133
"RunToolsTests",
133134
"ValidateApiDiff",
135+
"VerifyXamlCompilation",
134136
"ZipFiles"
135137
]
136138
}

Diff for: dirs.proj

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<ProjectReference Include="src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj" />
55
<ProjectReference Include="src/**/*.*proj" />
66
<ProjectReference Condition="'$(SkipBuildingSamples)' != 'True'" Include="samples/**/*.*proj" />
7-
<ProjectReference Condition="'$(SkipBuildingTests)' != 'True'" Include="tests/**/*.*proj" />
7+
<ProjectReference Condition="'$(SkipBuildingTests)' != 'True'" Include="tests/**/*.*proj" Exclude="tests/BuildTests/**" />
88
<ProjectReference Include="packages/**/*.*proj" />
99
<ProjectReference Remove="**/*.shproj" />
1010
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />

Diff for: nukebuild/Build.cs

+62
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ await Task.WhenAll(
360360

361361
Target CiAzureWindows => _ => _
362362
.DependsOn(Package)
363+
.DependsOn(VerifyXamlCompilation)
363364
.DependsOn(ZipFiles);
364365

365366
Target BuildToNuGetCache => _ => _
@@ -406,6 +407,67 @@ await Task.WhenAll(
406407
file.GenerateCppHeader());
407408
});
408409

410+
Target VerifyXamlCompilation => _ => _
411+
.DependsOn(CreateNugetPackages)
412+
.Executes(() =>
413+
{
414+
var buildTestsDirectory = RootDirectory / "tests" / "BuildTests";
415+
var artifactsDirectory = buildTestsDirectory / "artifacts";
416+
var nugetCacheDirectory = artifactsDirectory / "nuget-cache";
417+
418+
DeleteDirectory(artifactsDirectory);
419+
BuildTestsAndVerify("Debug");
420+
BuildTestsAndVerify("Release");
421+
422+
void BuildTestsAndVerify(string configuration)
423+
{
424+
var configName = configuration.ToLowerInvariant();
425+
426+
DotNetBuild(settings => settings
427+
.SetConfiguration(configuration)
428+
.SetProperty("AvaloniaVersion", Parameters.Version)
429+
.SetProperty("NuGetPackageRoot", nugetCacheDirectory)
430+
.SetPackageDirectory(nugetCacheDirectory)
431+
.SetProjectFile(buildTestsDirectory / "BuildTests.sln")
432+
.SetProcessArgumentConfigurator(arguments => arguments.Add("--nodeReuse:false")));
433+
434+
// Standard compilation - should have compiled XAML
435+
VerifyBuildTestAssembly("bin", "BuildTests");
436+
VerifyBuildTestAssembly("bin", "BuildTests.Android");
437+
VerifyBuildTestAssembly("bin", "BuildTests.Browser");
438+
VerifyBuildTestAssembly("bin", "BuildTests.Desktop");
439+
VerifyBuildTestAssembly("bin", "BuildTests.FSharp");
440+
VerifyBuildTestAssembly("bin", "BuildTests.iOS");
441+
VerifyBuildTestAssembly("bin", "BuildTests.WpfHybrid");
442+
443+
// Publish previously built project without rebuilding - should have compiled XAML
444+
PublishBuildTestProject("BuildTests.Desktop", noBuild: true);
445+
VerifyBuildTestAssembly("publish", "BuildTests.Desktop");
446+
447+
// Publish NativeAOT build, then run it - should not crash and have the expected output
448+
PublishBuildTestProject("BuildTests.NativeAot");
449+
var exeExtension = OperatingSystem.IsWindows() ? ".exe" : null;
450+
XamlCompilationVerifier.VerifyNativeAot(
451+
GetBuildTestOutputPath("publish", "BuildTests.NativeAot", exeExtension));
452+
453+
void PublishBuildTestProject(string projectName, bool? noBuild = null)
454+
=> DotNetPublish(settings => settings
455+
.SetConfiguration(configuration)
456+
.SetProperty("AvaloniaVersion", Parameters.Version)
457+
.SetProperty("NuGetPackageRoot", nugetCacheDirectory)
458+
.SetPackageDirectory(nugetCacheDirectory)
459+
.SetNoBuild(noBuild)
460+
.SetProject(buildTestsDirectory / projectName / (projectName + ".csproj"))
461+
.SetProcessArgumentConfigurator(arguments => arguments.Add("--nodeReuse:false")));
462+
463+
void VerifyBuildTestAssembly(string folder, string projectName)
464+
=> XamlCompilationVerifier.VerifyAssemblyCompiledXaml(
465+
GetBuildTestOutputPath(folder, projectName, ".dll"));
466+
467+
AbsolutePath GetBuildTestOutputPath(string folder, string projectName, string extension)
468+
=> artifactsDirectory / folder / projectName / configName / (projectName + extension);
469+
}
470+
});
409471

410472
public static int Main() =>
411473
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)

Diff for: nukebuild/XamlCompilationVerifier.cs

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Linq;
3+
using Mono.Cecil;
4+
using Nuke.Common.Tooling;
5+
using Serilog;
6+
7+
internal static class XamlCompilationVerifier
8+
{
9+
public static void VerifyAssemblyCompiledXaml(string assemblyPath)
10+
{
11+
const string avaloniaResourcesTypeName = "CompiledAvaloniaXaml.!AvaloniaResources";
12+
const string mainViewTypeName = "BuildTests.MainView";
13+
const string populateMethodName = "!XamlIlPopulate";
14+
15+
using var assembly = AssemblyDefinition.ReadAssembly(assemblyPath);
16+
17+
if (assembly.MainModule.GetType(avaloniaResourcesTypeName) is null)
18+
{
19+
throw new InvalidOperationException(
20+
$"Assembly {assemblyPath} is missing type {avaloniaResourcesTypeName}");
21+
}
22+
23+
if (assembly.MainModule.GetType(mainViewTypeName) is not { } mainViewType)
24+
{
25+
throw new InvalidOperationException(
26+
$"Assembly {assemblyPath} is missing type {mainViewTypeName}");
27+
}
28+
29+
if (!mainViewType.Methods.Any(method => method.Name == populateMethodName))
30+
{
31+
throw new InvalidOperationException(
32+
$"Assembly {assemblyPath} is missing method {populateMethodName} on {mainViewTypeName}");
33+
}
34+
35+
Log.Information($"Assembly {assemblyPath} correctly has compiled XAML");
36+
}
37+
38+
public static void VerifyNativeAot(string programPath)
39+
{
40+
const string expectedOutput = "Hello from AOT";
41+
42+
using var process = ProcessTasks.StartProcess(programPath, string.Empty);
43+
44+
process.WaitForExit();
45+
process.AssertZeroExitCode();
46+
47+
var output = process.Output.Select(o => o.Text).FirstOrDefault();
48+
if (output != expectedOutput)
49+
{
50+
throw new InvalidOperationException(
51+
$"{programPath} returned text \"{output}\", expected \"{expectedOutput}\"");
52+
}
53+
54+
Log.Information($"Native program {programPath} correctly has compiled XAML");
55+
}
56+
}

Diff for: packages/Avalonia/AvaloniaBuildTasks.targets

+24-73
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363

6464
<PropertyGroup>
6565
<BuildAvaloniaResourcesDependsOn>$(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache</BuildAvaloniaResourcesDependsOn>
66-
<CompileAvaloniaXamlDependsOn>$(CompileAvaloniaXamlDependsOn);FindReferenceAssembliesForReferences;PrepareToCompileAvaloniaXaml</CompileAvaloniaXamlDependsOn>
66+
<CompileAvaloniaXamlDependsOn>$(CompileAvaloniaXamlDependsOn);FindReferenceAssembliesForReferences</CompileAvaloniaXamlDependsOn>
6767
</PropertyGroup>
6868

6969
<Target Name="_GenerateAvaloniaResourcesDependencyCache" BeforeTargets="GenerateAvaloniaResources">
@@ -107,34 +107,39 @@
107107
</ItemGroup>
108108
</Target>
109109

110-
<Target Name="PrepareToCompileAvaloniaXaml">
110+
<!-- Adjust CoreCompile with our XAML inputs and compiler -->
111+
<Target Name="AvaloniaPrepareCoreCompile" BeforeTargets="CoreCompile">
111112
<PropertyGroup>
112-
<AvaloniaXamlIlVerifyIl Condition="'$(AvaloniaXamlIlVerifyIl)' == ''">false</AvaloniaXamlIlVerifyIl>
113-
<AvaloniaXamlIlDebuggerLaunch Condition="'$(AvaloniaXamlIlDebuggerLaunch)' == ''">false</AvaloniaXamlIlDebuggerLaunch>
114-
<AvaloniaXamlVerboseExceptions Condition="'$(AvaloniaXamlVerboseExceptions)' == ''">false</AvaloniaXamlVerboseExceptions>
115-
<_AvaloniaHasCompiledXaml>true</_AvaloniaHasCompiledXaml>
113+
<TargetsTriggeredByCompilation>$(TargetsTriggeredByCompilation);CompileAvaloniaXaml</TargetsTriggeredByCompilation>
116114
</PropertyGroup>
117-
118115
<ItemGroup>
119-
<IntermediateAssembly Update="*" AvaloniaCompileOutput="%(RelativeDir)Avalonia\%(Filename)%(Extension)"/>
120-
<_DebugSymbolsIntermediatePath Update="*" AvaloniaCompileOutput="%(RelativeDir)Avalonia\%(Filename)%(Extension)"/>
121-
<IntermediateRefAssembly Update="*" AvaloniaCompileOutput="%(RelativeDir)Avalonia\%(Filename)%(Extension)"/>
122-
<CompileAvaloniaXamlInputs Include="@(IntermediateAssembly);@(_DebugSymbolsIntermediatePath);@(IntermediateRefAssembly)"/>
123-
<CompileAvaloniaXamlOutputs Include="@(CompileAvaloniaXamlInputs->'%(AvaloniaCompileOutput)')"/>
124-
<CompileAvaloniaXamlInputs Include="@(AvaloniaResource);@(AvaloniaXaml)"/>
125-
126-
<FileWrites Include="@(CompileAvaloniaXamlOutputs)"/>
116+
<CustomAdditionalCompileInputs Include="@(AvaloniaResource)" />
117+
<CustomAdditionalCompileInputs Include="@(AvaloniaXaml)" />
127118
</ItemGroup>
128119
</Target>
129120

121+
<Target Name="AvaloniaCollectUpToDateCheckInputDesignTime" BeforeTargets="CollectUpToDateCheckInputDesignTime">
122+
<UpToDateCheckInput Include="@(AvaloniaResource)" />
123+
<UpToDateCheckInput Include="@(AvaloniaXaml)" />
124+
</Target>
125+
130126
<Target
131127
Name="CompileAvaloniaXaml"
132-
AfterTargets="AfterCompile"
133128
DependsOnTargets="$(CompileAvaloniaXamlDependsOn)"
134-
Inputs="@(CompileAvaloniaXamlInputs)"
135-
Outputs="@(CompileAvaloniaXamlOutputs)"
136129
Condition="'@(AvaloniaResource)@(AvaloniaXaml)' != '' AND $(DesignTimeBuild) != true AND $(EnableAvaloniaXamlCompilation) != false">
137130

131+
<PropertyGroup>
132+
<AvaloniaXamlIlVerifyIl Condition="'$(AvaloniaXamlIlVerifyIl)' == ''">false</AvaloniaXamlIlVerifyIl>
133+
<AvaloniaXamlIlDebuggerLaunch Condition="'$(AvaloniaXamlIlDebuggerLaunch)' == ''">false</AvaloniaXamlIlDebuggerLaunch>
134+
<AvaloniaXamlVerboseExceptions Condition="'$(AvaloniaXamlVerboseExceptions)' == ''">false</AvaloniaXamlVerboseExceptions>
135+
</PropertyGroup>
136+
137+
<ItemGroup>
138+
<IntermediateAssembly Update="*" AvaloniaCompileOutput="%(FullPath)"/>
139+
<_DebugSymbolsIntermediatePath Update="*" AvaloniaCompileOutput="%(FullPath)"/>
140+
<IntermediateRefAssembly Update="*" AvaloniaCompileOutput="%(FullPath)"/>
141+
</ItemGroup>
142+
138143
<!--
139144
$(IntermediateOutputPath)/Avalonia/references is using from AvaloniaVS for retrieve library references.
140145
-->
@@ -160,60 +165,6 @@
160165
AnalyzerConfigFiles="@(EditorConfigFiles)"/>
161166
</Target>
162167

163-
<Target Name="InjectAvaloniaXamlOutput" DependsOnTargets="PrepareToCompileAvaloniaXaml" AfterTargets="CompileAvaloniaXaml" BeforeTargets="CopyFilesToOutputDirectory;BuiltProjectOutputGroup;ComputeResolvedFilesToPublishList"
164-
Condition="'@(AvaloniaResource)@(AvaloniaXaml)' != '' AND $(EnableAvaloniaXamlCompilation) != false">
165-
<ItemGroup>
166-
<_AvaloniaXamlCompiledAssembly Include="@(IntermediateAssembly->Metadata('AvaloniaCompileOutput'))"/>
167-
<IntermediateAssembly Remove="@(IntermediateAssembly)"/>
168-
<IntermediateAssembly Include="@(_AvaloniaXamlCompiledAssembly)"/>
169-
170-
<_AvaloniaXamlCompiledRefAssembly Include="@(IntermediateRefAssembly->Metadata('AvaloniaCompileOutput'))"/>
171-
<IntermediateRefAssembly Remove="@(IntermediateRefAssembly)"/>
172-
<IntermediateRefAssembly Include="@(_AvaloniaXamlCompiledRefAssembly)"/>
173-
174-
<_AvaloniaXamlCompiledSymbols Include="@(_DebugSymbolsIntermediatePath->Metadata('AvaloniaCompileOutput'))"/>
175-
<_DebugSymbolsIntermediatePath Remove="@(_DebugSymbolsIntermediatePath)"/>
176-
<_DebugSymbolsIntermediatePath Include="@(_AvaloniaXamlCompiledSymbols)"/>
177-
178-
<!-- ClickOnce takes a copy of @(IntermediateAssembly) during the evaluation phase -->
179-
<_DeploymentManifestEntryPoint Remove="@(_DeploymentManifestEntryPoint)" />
180-
<_DeploymentManifestEntryPoint Include="@(_AvaloniaXamlCompiledAssembly)">
181-
<TargetPath>$(TargetFileName)</TargetPath>
182-
</_DeploymentManifestEntryPoint>
183-
</ItemGroup>
184-
</Target>
185-
186-
<!--
187-
For some reason the IL Compiler hardcodes $(IntermediateOutputPath)$(TargetName)$(TargetExt)
188-
instead of using @(IntermediateAssembly), change that to our assembly.
189-
This is fixed in .NET 9.0 (https://github.com/dotnet/runtime/pull/99732)
190-
-->
191-
<Target Name="InjectIlcAvaloniaXamlOutput"
192-
DependsOnTargets="InjectAvaloniaXamlOutput"
193-
AfterTargets="ComputeIlcCompileInputs"
194-
BeforeTargets="PrepareForILLink"
195-
Condition="$([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '9.0')) AND '@(AvaloniaResource)@(AvaloniaXaml)' != '' AND $(EnableAvaloniaXamlCompilation) != false">
196-
<ItemGroup>
197-
<ManagedBinary Remove="$(IntermediateOutputPath)$(TargetName)$(TargetExt)" />
198-
<ManagedBinary Include="@(_AvaloniaXamlCompiledAssembly)" />
199-
200-
<IlcCompileInput Remove="$(IntermediateOutputPath)$(TargetName)$(TargetExt)" />
201-
<IlcCompileInput Include="@(_AvaloniaXamlCompiledAssembly)" />
202-
</ItemGroup>
203-
</Target>
204-
205-
<Target Name="Avalonia_CollectUpToDateCheckOutputDesignTime" Condition="'@(AvaloniaResource)@(AvaloniaXaml)' != '' AND $(EnableAvaloniaXamlCompilation) != false"
206-
BeforeTargets="CollectUpToDateCheckOutputDesignTime" DependsOnTargets="PrepareToCompileAvaloniaXaml">
207-
<ItemGroup>
208-
<UpToDateCheckOutput Include="@(CompileAvaloniaXamlOutputs)"/>
209-
</ItemGroup>
210-
</Target>
211-
212-
<ItemGroup>
213-
<UpToDateCheckInput Include="@(AvaloniaResource)" />
214-
<UpToDateCheckInput Include="@(AvaloniaXaml)" />
215-
</ItemGroup>
216-
217168
<PropertyGroup>
218169
<AvaloniaFilePreviewDependsOn Condition="'$(SkipBuild)'!='True'">Build</AvaloniaFilePreviewDependsOn>
219170
</PropertyGroup>
@@ -252,7 +203,7 @@
252203
Name="AvaloniaDeleteRefAssemblyBeforeOutputCopy"
253204
BeforeTargets="CopyFilesToOutputDirectory"
254205
Condition="
255-
'$(_AvaloniaHasCompiledXaml)' == 'true' and
206+
'@(AvaloniaResource)@(AvaloniaXaml)' != '' and
256207
'$(TargetRefPath)' != '' and
257208
'$(ProduceReferenceAssembly)' == 'true' and
258209
('$(CopyBuildOutputToOutputDirectory)' == '' or '$(CopyBuildOutputToOutputDirectory)' == 'true') and

Diff for: src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs

+11-4
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,25 @@ public bool Execute()
5151

5252
private static void CopyAndTouch(string source, string destination, bool shouldExist = true)
5353
{
54-
if (!File.Exists(source))
54+
var normalizedSource = Path.GetFullPath(source);
55+
var normalizedDestination = Path.GetFullPath(destination);
56+
57+
if (!File.Exists(normalizedSource))
5558
{
5659
if (shouldExist)
5760
{
58-
throw new FileNotFoundException($"Could not copy file '{source}'. File does not exist.");
61+
throw new FileNotFoundException($"Could not copy file '{normalizedSource}'. File does not exist.");
5962
}
6063

6164
return;
6265
}
6366

64-
File.Copy(source, destination, overwrite: true);
65-
File.SetLastWriteTimeUtc(destination, DateTime.UtcNow);
67+
if (normalizedSource != normalizedDestination)
68+
{
69+
File.Copy(normalizedSource, normalizedDestination, overwrite: true);
70+
}
71+
72+
File.SetLastWriteTimeUtc(normalizedDestination, DateTime.UtcNow);
6673
}
6774

6875
[Required]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0-android</TargetFramework>
6+
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
7+
<Nullable>enable</Nullable>
8+
<ApplicationId>com.Avalonia.BuildTests</ApplicationId>
9+
<ApplicationVersion>1</ApplicationVersion>
10+
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
11+
<AndroidPackageFormat>apk</AndroidPackageFormat>
12+
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
13+
</PropertyGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="Avalonia.Android" />
17+
</ItemGroup>
18+
19+
<Import Project="../IncludeBuildTestsAvaloniaItems.props" />
20+
21+
</Project>

Diff for: tests/BuildTests/BuildTests.Android/MainActivity.cs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Android.App;
2+
using Android.Content.PM;
3+
using Avalonia.Android;
4+
5+
namespace BuildTests.Android;
6+
7+
[Activity(
8+
Label = "BuildTests.Android",
9+
MainLauncher = true,
10+
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
11+
public class MainActivity : AvaloniaMainActivity<App>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
3+
<uses-permission android:name="android.permission.INTERNET" />
4+
<application android:label="BuildTests" />
5+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0-browser</TargetFramework>
5+
<OutputType>Exe</OutputType>
6+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Avalonia.Browser" />
12+
</ItemGroup>
13+
14+
<Import Project="../IncludeBuildTestsAvaloniaItems.props" />
15+
16+
</Project>

0 commit comments

Comments
 (0)