Skip to content

Commit

Permalink
fix: always generate Icon Extension
Browse files Browse the repository at this point in the history
  • Loading branch information
ajpinedam committed Jan 17, 2025
1 parent dfcb976 commit afa0e5a
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 142 deletions.
96 changes: 70 additions & 26 deletions src/.nuspec/Uno.Resizetizer.targets
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</ItemGroup>

<PropertyGroup>
<_UnoResizetizerTaskAssemblyName>$(MSBuildThisFileDirectory)netstandard2.0\Uno.Resizetizer_v0.dll</_UnoResizetizerTaskAssemblyName>
<_UnoResizetizerTaskAssemblyName>$(MSBuildThisFileDirectory)netstandard2.0\Uno.Resizetizer_v0.dll</_UnoResizetizerTaskAssemblyName>
</PropertyGroup>

<UsingTask
Expand Down Expand Up @@ -48,10 +48,6 @@
AssemblyFile="$(_UnoResizetizerTaskAssemblyName)"
TaskName="Uno.Resizetizer.GenerateWasmSplashAssets_v0"/>

<UsingTask
AssemblyFile="$(_UnoResizetizerTaskAssemblyName)"
TaskName="Uno.Resizetizer.WindowIconGeneratorTask_V0"/>

<UsingTask
AssemblyFile="$(_UnoResizetizerTaskAssemblyName)"
TaskName="Uno.Resizetizer.CleanupAssetsTask_v0"/>
Expand Down Expand Up @@ -231,6 +227,8 @@
_ComputeAndroidResourcePaths;
$(UnoResizetizeCollectItemsAfterTargets);
UnoAssetsGeneration;
GenerateMSBuildEditorConfigFileShouldRun;
GenerateMSBuildEditorConfigFileCore;
</UnoResizetizeCollectItemsBeforeTargets>

<UnoResizetizeAfterTargets>
Expand Down Expand Up @@ -273,7 +271,8 @@


<Target Name="ValidateAvailableItems"
BeforeTargets="UnoResizetizeCollectItems">
BeforeTargets="UnoResizetizeCollectItems"
DependsOnTargets="_ResizetizerInitialize">

<Warning
Condition="'%(UnoIcon.Link)' != ''"
Expand Down Expand Up @@ -417,6 +416,7 @@
WriteOnlyWhenDifferent="true"/>

<ItemGroup>
<AdditionalFiles Include="$(_UnoResizetizerInputsFile)"/>
<FileWrites Include="$(_UnoResizetizerInputsFile)"/>
<FileWrites Include="$(_UnoSplashInputsFile)"/>
</ItemGroup>
Expand Down Expand Up @@ -512,7 +512,7 @@

<PropertyGroup>
<!-- If the PWA manifest is empty we can try to find it on disk -->
<_UnoResizetizerPwaManifest Condition="exists($(WasmPWAManifestFile))">$(_UnoIntermediateAppIcon)..\$([System.IO.Path]::GetFileName($(WasmPWAManifestFile)))</_UnoResizetizerPwaManifest>
<_UnoResizetizerPwaManifest>$(_UnoIntermediateAppIcon)..\$([System.IO.Path]::GetFileName($(WasmPWAManifestFile)))</_UnoResizetizerPwaManifest>
<UnoResizetizerPwaManifest Condition="
'$(UnoResizetizerPwaManifest)'=='' and
Exists('$(_UnoResizetizerPwaManifest)')">$(_UnoResizetizerPwaManifest)</UnoResizetizerPwaManifest>
Expand Down Expand Up @@ -588,25 +588,12 @@
</ItemGroup>
</Target>

<Target Name="_GenerateWindowUnoIconExtension"
Condition="'$(_UnoResizetizerIsCompatibleApp)' == 'True'"
BeforeTargets="Build;CoreCompile;XamlPreCompile"
Inputs="@(UnoIcon)"
Outputs="@(_WindowIconExtension)">

<PropertyGroup>
<WindowTitle Condition="'$(WindowTitle)' == ''">$([MSBuild]::ValueOrDefault('$(ApplicationTitle)', '$(AssemblyName)'))</WindowTitle>
</PropertyGroup>

<WindowIconGeneratorTask_v0
IntermediateOutputDirectory="$(_UnoResizetizerIntermediateOutputRoot)"
WindowTitle="$(WindowTitle)"
UnoIcons="@(UnoIcon)">
<Output ItemName="GeneratedFile"
TaskParameter="GeneratedClass"/>
<Output ItemName="FilesWrite"
TaskParameter="GeneratedClass"/>
</WindowIconGeneratorTask_v0>
<Target Name="SetUnoImage"
BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun;GenerateMSBuildEditorConfigFileCore;ResolveFrameworkReferencesDesignTime;ResolveProjectReferencesDesignTime2;CollectAnalyzersDesignTime;GetAndroidDependencies">
<CreateProperty Value="@(UnoIcon, ',')">
<Output TaskParameter="Value"
PropertyName="UnoResizetizerIcon" />
</CreateProperty>
</Target>

<Target Name="_AddResizetizerWindowIconExtensions"
Expand All @@ -618,4 +605,61 @@
</ItemGroup>
</Target>

<!--
This is used to get and copy the native runtime assets. This is a workaround as the assets aren't properly loading
Executed before UnoResizetizeImages for TargetFrameworks=='' and TargetFramework!=''
Executed before _SetBuildInnerTarget;_ComputeTargetFrameworkItems for TargetFrameworks!='' and TargetFramework==''
-->
<Target Name="_ResizetizerInitialize"
BeforeTargets="UnoResizetizeImages;_SetBuildInnerTarget;_ComputeTargetFrameworkItems">
<PropertyGroup>
<_ResizetizerRuntimeIdentifier>$(NETCoreSdkPortableRuntimeIdentifier)</_ResizetizerRuntimeIdentifier>
<!-- NOTE: We may need to adjust this in the future if we end up with assets compiled for osx-arm64 and osx-x64 -->
<_ResizetizerRuntimeIdentifier Condition=" $(_ResizetizerRuntimeIdentifier.Contains('osx')) ">osx</_ResizetizerRuntimeIdentifier>
<_ResizetizerRuntimeIdentifierDirectory>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'netstandard2.0', 'runtimes', '$(_ResizetizerRuntimeIdentifier)'))</_ResizetizerRuntimeIdentifierDirectory>
<_ResizetizerRuntimeAssetsOutput>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'netstandard2.0'))</_ResizetizerRuntimeAssetsOutput>
</PropertyGroup>

<ItemGroup>
<_ResiztizerRuntimeAssets Include="$(_ResizetizerRuntimeIdentifierDirectory)\**\*"
OutputDirectory="$(_ResizetizerRuntimeAssetsOutput)" />
</ItemGroup>

<Error Text="No Runtime was found."
Condition="$(_ResizetizerRuntimeIdentifier) == ''" />

<Error Text="The selected runtime directory does not exist"
Condition="!Exists('$(_ResizetizerRuntimeIdentifierDirectory)')" />

<!--
Try to copy files with one retry, fail fast and ignore the error if this happens, then retry
copying the files in a second time. This is present to avoid a race condition during
cross-targeted builds when this current target is executed concurrently.
When running, the Copy task may detect that files are not present for two
threads/processes, then assume it's safe to copy, while only one of them can do
so. Later on, the copied files may be locked by dotnet/msbuild and the other copy
instance never finishes sucessfully, regardless of SkipUnchangedFiles.
-->
<Copy SourceFiles="@(_ResiztizerRuntimeAssets)"
DestinationFiles="@(_ResiztizerRuntimeAssets->'%(OutputDirectory)\%(Filename)%(Extension)')"
SkipUnchangedFiles="true"
OverwriteReadOnlyFiles="true"
Condition="Exists('$(_ResizetizerRuntimeIdentifierDirectory)')"
UseHardlinksIfPossible="true"
ContinueOnError="true"
Retries="1"
UseSymboliclinksIfPossible="true" />

<Copy SourceFiles="@(_ResiztizerRuntimeAssets)"
DestinationFiles="@(_ResiztizerRuntimeAssets->'%(OutputDirectory)\%(Filename)%(Extension)')"
SkipUnchangedFiles="true"
OverwriteReadOnlyFiles="true"
Condition="Exists('$(_ResizetizerRuntimeIdentifierDirectory)')"
UseHardlinksIfPossible="true"
UseSymboliclinksIfPossible="true" />

</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<AssemblyName>Uno.Resizetizer.Generators</AssemblyName>
<DevelopmentDependency>true</DevelopmentDependency>
<BuildOutputTargetFolder>analyzers/dotnet/cs</BuildOutputTargetFolder>
<LangVersion>latest</LangVersion>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PolySharpIncludeGeneratedTypes>
System.Runtime.CompilerServices.IsExternalInit;
System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;
</PolySharpIncludeGeneratedTypes>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AvantiPoint.CodeGenHelpers" Version="1.6.20">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
183 changes: 183 additions & 0 deletions src/Resizetizer/Resizetizer.Generators/WindowTitleGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using CodeGenHelpers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;

namespace Resizetizer.Generators;

[Generator(LanguageNames.CSharp)]
internal sealed class WindowTitleGenerator : IIncrementalGenerator
{
private const string UnoResizetizerIcon = nameof(UnoResizetizerIcon);
private const string IsUnoHead = nameof(IsUnoHead);

public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Get the AnalyzerConfigOptionsProvider
var optionsProvider = context.AnalyzerConfigOptionsProvider;
var assemblyNameProvider = context.CompilationProvider.Select((compilation, _) => compilation.Assembly.Name);
var additionalTextsProvider = context.AdditionalTextsProvider;

var extensionPropertiesProvider = optionsProvider.Combine(assemblyNameProvider).Select((x, cancellationToken) =>
{
var (options, assemblyName) = x;
if (!GetProperty(options.GlobalOptions, IsUnoHead))
{
return null;
}

var rootNamespace = GetPropertyValue(options.GlobalOptions, "RootNamespace");
var windowTitle = GetPropertyValue(options.GlobalOptions, "ApplicationTitle");
if (string.IsNullOrEmpty(windowTitle))
{
windowTitle = assemblyName;
}

return string.IsNullOrEmpty(rootNamespace) || string.IsNullOrEmpty(windowTitle) ? null : new ExtensionPropertiesContext(rootNamespace, windowTitle);
});

// Combine optionsProvider and compilationProvider
var iconNameProvider = additionalTextsProvider
.Where(x => Path.GetFileName(x.Path).Equals("UnoImage.inputs", StringComparison.InvariantCultureIgnoreCase))
.Select((additionalText, cancellationToken) =>
{
var sourceText = additionalText.GetText(cancellationToken);
return ParseFile(sourceText.ToString());
})
.Where(x => !string.IsNullOrEmpty(x))
.Select((x, _) => Path.GetFileNameWithoutExtension(x));

// Define the source generator logic
var sourceCodeProvider = iconNameProvider.Combine(extensionPropertiesProvider).Select((x, _) =>
{
var (iconName, coreContext) = x;
if (string.IsNullOrEmpty(iconName) || string.IsNullOrEmpty(coreContext?.RootNamespace) || string.IsNullOrEmpty(coreContext?.WindowTitle))
return null;

return new ExtensionGenerationContext(coreContext.RootNamespace, iconName, coreContext.WindowTitle);
}).Where(result => result != null);

// Register the source generator logic to add the generated source code
context.RegisterSourceOutput(sourceCodeProvider, (sourceContext, extensionContext) =>
{
if (!string.IsNullOrEmpty(extensionContext.WindowTitle))
{
AddSource(sourceContext, GenerateLegacyNamespaceCompat());
AddSource(sourceContext, GenerateWindowTitleExtension(extensionContext.RootNamespace, extensionContext.IconName, extensionContext.WindowTitle));
}
});
}

internal record ExtensionPropertiesContext(string RootNamespace, string WindowTitle);

internal record ExtensionGenerationContext(string RootNamespace, string IconName, string WindowTitle);

private static string ParseFile(string content)
{
// Split the content into lines
var lines = content.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);

foreach (var line in lines)
{
// Split the line into key-value pairs
var properties = line.Split(';')
.Select(property => property.Split('='))
.ToDictionary(parts => parts[0], parts => parts.Length > 1 ? parts[1] : null);

// Check if IsAppIcon is true
if (properties.TryGetValue("IsAppIcon", out var isAppIcon) && bool.TryParse(isAppIcon, out var isAppIconValue) && isAppIconValue)
{
// Return the file path
if (properties.TryGetValue("File", out var filePath))
{
return filePath;
}
}
}

// Return null if no app icon is found
return null;
}

private static ClassBuilder GenerateWindowTitleExtension(string rootNamespace, string iconName, string windowTitle)
{
var builder = CodeBuilder.Create(rootNamespace)
.AddClass("WindowExtensions")
.MakeStaticClass()
.WithSummary(@"Extension methods for the <see cref=""global::Microsoft.UI.Xaml.Window"" /> class.");

builder.AddMethod("SetWindowIcon")
.AddParameter("this global::Microsoft.UI.Xaml.Window", "window")
.MakeStaticMethod()
.MakePublicMethod()
.WithSummary(@"This will set the Window Icon for the given <see cref=""global::Microsoft.UI.Xaml.Window"" /> using the provided UnoIcon.")
.WithBody(w =>
{
w.AppendUnindentedLine("#if WINDOWS && !HAS_UNO");
w.AppendLine("var hWnd = global::WinRT.Interop.WindowNative.GetWindowHandle(window);");
w.NewLine();
w.AppendLine("// Retrieve the WindowId that corresponds to hWnd.");
w.AppendLine("global::Microsoft.UI.WindowId windowId = global::Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd);");
w.NewLine();
w.AppendLine("// Lastly, retrieve the AppWindow for the current (XAML) WinUI 3 window.");
w.AppendLine("global::Microsoft.UI.Windowing.AppWindow appWindow = global::Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);");
w.AppendLine($@"appWindow.SetIcon(""{iconName}.ico"");");
w.NewLine();
w.AppendLine("// Set the Window Title Only if it has the Default WinUI Desktop value and we are running Unpackaged");
// We're no longer checking for IsPackaged as this seems to be needed when Packaged as well.
w.If(@"appWindow.Title == ""WinUI Desktop""")
.WithBody(b =>
{
b.AppendLine($@"appWindow.Title = ""{windowTitle}"";");
})
.EndIf();
w.AppendUnindentedLine("#endif");
});

// NOTE: This method has been removed as it seems WinUI isn't setting the title when Packaged. Keeping in case we need this in the future.
//builder.AddMethod("IsPackaged")
// .WithReturnType("bool")
// .MakePrivateMethod()
// .MakeStaticMethod()
// .WithBody(w =>
// {
// using (w.Block("try"))
// {
// w.AppendLine("return global::Windows.ApplicationModel.Package.Current != null;");
// }
// using (w.Block("catch"))
// {
// w.AppendLine("return false;");
// }
// });

return builder;
}

private static string GetPropertyValue(AnalyzerConfigOptions options, string key) =>
options.TryGetValue($"build_property.{key}", out var value) ? value : string.Empty;

private static bool GetProperty(AnalyzerConfigOptions options, string key) =>
bool.TryParse(GetPropertyValue(options, key), out var result) && result;

private static bool HasUnoIcon(AnalyzerConfigOptions options, out string unoIcon)
{
unoIcon = GetPropertyValue(options, UnoResizetizerIcon);
return !string.IsNullOrEmpty(unoIcon) && !unoIcon.Contains(",");
}

private static ClassBuilder GenerateLegacyNamespaceCompat()
{
return CodeBuilder.Create("Uno.Resizetizer")
.AddClass("__LegacyResizetizerSupport__")
.WithSummary("This is added to ensure the Uno.Resizetizer namespace is present to avoid breaking any applications.")
.MakeStaticClass();
}

private static void AddSource(SourceProductionContext context, ClassBuilder builder) =>
context.AddSource($"{builder.FullyQualifiedName}.g.cs", SourceText.From(builder.Build(), Encoding.UTF8));
}
Loading

0 comments on commit afa0e5a

Please sign in to comment.