-
Notifications
You must be signed in to change notification settings - Fork 715
Add Aspire.Hosting.Maui (.NET MAUI) Mac Catalyst integration #12342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Aspire.Hosting.Maui; | ||
|
|
||
| /// <summary> | ||
| /// Marker interface for MAUI platform-specific resources (Windows, Android, iOS, Mac Catalyst). | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This interface is used to identify resources that represent a specific platform instance | ||
| /// of a MAUI application, allowing for common handling across all MAUI platforms. | ||
| /// </remarks> | ||
| internal interface IMauiPlatformResource | ||
| { | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
| using Aspire.Hosting.Maui; | ||
|
|
||
| namespace Aspire.Hosting; | ||
|
|
||
| /// <summary> | ||
| /// Provides extension methods for adding Mac Catalyst platform resources to MAUI projects. | ||
| /// </summary> | ||
| public static class MauiMacCatalystExtensions | ||
| { | ||
| /// <summary> | ||
| /// Adds a Mac Catalyst device resource to run the MAUI application on the macOS platform. | ||
| /// </summary> | ||
| /// <param name="builder">The MAUI project resource builder.</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
| /// <remarks> | ||
| /// This method creates a new Mac Catalyst platform resource that will run the MAUI application | ||
| /// targeting the Mac Catalyst platform using <c>dotnet run</c>. The resource does not auto-start | ||
| /// and must be explicitly started from the dashboard by clicking the start button. | ||
| /// <para> | ||
| /// The resource name will default to "{projectName}-maccatalyst". | ||
| /// </para> | ||
| /// </remarks> | ||
| /// <example> | ||
| /// Add a Mac Catalyst device to a MAUI project: | ||
| /// <code lang="csharp"> | ||
| /// var builder = DistributedApplication.CreateBuilder(args); | ||
| /// | ||
| /// var maui = builder.AddMauiProject("mauiapp", "../MyMauiApp/MyMauiApp.csproj"); | ||
| /// var macCatalystDevice = maui.AddMacCatalystDevice(); | ||
| /// | ||
| /// builder.Build().Run(); | ||
| /// </code> | ||
| /// </example> | ||
| public static IResourceBuilder<MauiMacCatalystPlatformResource> AddMacCatalystDevice( | ||
| this IResourceBuilder<MauiProjectResource> builder) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
|
|
||
| var name = $"{builder.Resource.Name}-maccatalyst"; | ||
| return builder.AddMacCatalystDevice(name); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a Mac Catalyst device resource to run the MAUI application on the macOS platform with a specific name. | ||
| /// </summary> | ||
| /// <param name="builder">The MAUI project resource builder.</param> | ||
| /// <param name="name">The name of the Mac Catalyst device resource.</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
| /// <remarks> | ||
| /// This method creates a new Mac Catalyst platform resource that will run the MAUI application | ||
| /// targeting the Mac Catalyst platform using <c>dotnet run</c>. The resource does not auto-start | ||
| /// and must be explicitly started from the dashboard by clicking the start button. | ||
| /// <para> | ||
| /// Multiple Mac Catalyst device resources can be added to the same MAUI project if needed, each with | ||
| /// a unique name. | ||
| /// </para> | ||
| /// </remarks> | ||
| /// <example> | ||
| /// Add multiple Mac Catalyst devices to a MAUI project: | ||
| /// <code lang="csharp"> | ||
| /// var builder = DistributedApplication.CreateBuilder(args); | ||
| /// | ||
| /// var maui = builder.AddMauiProject("mauiapp", "../MyMauiApp/MyMauiApp.csproj"); | ||
| /// var macCatalystDevice1 = maui.AddMacCatalystDevice("maccatalyst-device-1"); | ||
| /// var macCatalystDevice2 = maui.AddMacCatalystDevice("maccatalyst-device-2"); | ||
| /// | ||
| /// builder.Build().Run(); | ||
| /// </code> | ||
| /// </example> | ||
| public static IResourceBuilder<MauiMacCatalystPlatformResource> AddMacCatalystDevice( | ||
| this IResourceBuilder<MauiProjectResource> builder, | ||
| [ResourceName] string name) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(name); | ||
|
|
||
| // Check if a Mac Catalyst device with this name already exists in the application model | ||
| var existingMacCatalystDevices = builder.ApplicationBuilder.Resources | ||
| .OfType<MauiMacCatalystPlatformResource>() | ||
| .FirstOrDefault(r => r.Parent == builder.Resource && | ||
| string.Equals(r.Name, name, StringComparisons.ResourceName)); | ||
|
Comment on lines
+81
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't actually need to two this check (in fact the check is not quite right). You can just add the resource to the model and it will do a uniqueness check on the name. You cannot have two resources with the same name even if they differ by type. Change this for the Windows resource too - I didn't pick it up in review last time ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in commit 669549a |
||
|
|
||
| if (existingMacCatalystDevices is not null) | ||
| { | ||
| throw new DistributedApplicationException( | ||
| $"Mac Catalyst device with name '{name}' already exists on MAUI project '{builder.Resource.Name}'. " + | ||
| $"Provide a unique name parameter when calling AddMacCatalystDevice() to add multiple Mac Catalyst devices."); | ||
| } | ||
|
|
||
| // Get the absolute project path and working directory | ||
| var (projectPath, workingDirectory) = MauiPlatformHelper.GetProjectPaths(builder); | ||
|
|
||
| var macCatalystResource = new MauiMacCatalystPlatformResource(name, builder.Resource); | ||
|
|
||
| var resourceBuilder = builder.ApplicationBuilder.AddResource(macCatalystResource) | ||
| .WithAnnotation(new MauiProjectMetadata(projectPath)) | ||
| .WithAnnotation(new ExecutableAnnotation | ||
| { | ||
| Command = "dotnet", | ||
| WorkingDirectory = workingDirectory | ||
| }); | ||
|
|
||
| // Configure the platform resource with common settings | ||
| MauiPlatformHelper.ConfigurePlatformResource( | ||
| resourceBuilder, | ||
| projectPath, | ||
| "maccatalyst", | ||
| "Mac Catalyst", | ||
| "net10.0-maccatalyst", | ||
| OperatingSystem.IsMacOS, | ||
| "Desktop", | ||
| "-p:OpenArguments=-W"); | ||
|
|
||
| return resourceBuilder; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
|
|
||
| namespace Aspire.Hosting.Maui; | ||
|
|
||
| /// <summary> | ||
| /// Represents a Mac Catalyst platform instance of a .NET MAUI project. | ||
| /// </summary> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <param name="parent">The parent MAUI project resource.</param> | ||
| /// <remarks> | ||
| /// This resource represents a MAUI application running on the Mac Catalyst platform. | ||
| /// The actual build and deployment happens when the resource is started, allowing for | ||
| /// incremental builds during development without blocking AppHost startup. | ||
| /// <para> | ||
| /// Use <see cref="MauiMacCatalystExtensions.AddMacCatalystDevice(IResourceBuilder{MauiProjectResource}, string?)"/> | ||
| /// to add this resource to a MAUI project. | ||
| /// </para> | ||
| /// </remarks> | ||
| public class MauiMacCatalystPlatformResource(string name, MauiProjectResource parent) | ||
| : ProjectResource(name), IResourceWithParent<MauiProjectResource>, IMauiPlatformResource | ||
| { | ||
| /// <summary> | ||
| /// Gets the parent MAUI project resource. | ||
| /// </summary> | ||
| public MauiProjectResource Parent { get; } = parent ?? throw new ArgumentNullException(nameof(parent)); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
| using Aspire.Hosting.Lifecycle; | ||
| using Aspire.Hosting.Maui.Annotations; | ||
| using Aspire.Hosting.Maui.Lifecycle; | ||
| using Aspire.Hosting.Maui.Utilities; | ||
| using Aspire.Hosting.Utils; | ||
|
|
||
| namespace Aspire.Hosting.Maui; | ||
|
|
||
| /// <summary> | ||
| /// Helper methods for adding platform-specific MAUI device resources. | ||
| /// </summary> | ||
| internal static class MauiPlatformHelper | ||
| { | ||
| /// <summary> | ||
| /// Gets the absolute project path and working directory from a MAUI project resource. | ||
| /// </summary> | ||
| /// <param name="builder">The MAUI project resource builder.</param> | ||
| /// <returns>A tuple containing the absolute project path and working directory.</returns> | ||
| internal static (string ProjectPath, string WorkingDirectory) GetProjectPaths(IResourceBuilder<MauiProjectResource> builder) | ||
| { | ||
| var projectPath = builder.Resource.ProjectPath; | ||
| if (!Path.IsPathRooted(projectPath)) | ||
| { | ||
| projectPath = PathNormalizer.NormalizePathForCurrentPlatform( | ||
| Path.Combine(builder.ApplicationBuilder.AppHostDirectory, projectPath)); | ||
| } | ||
|
|
||
| var workingDirectory = Path.GetDirectoryName(projectPath) | ||
| ?? throw new InvalidOperationException($"Unable to determine directory from project path: {projectPath}"); | ||
|
|
||
| return (projectPath, workingDirectory); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Configures a platform resource with common settings and TFM validation. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of platform resource.</typeparam> | ||
| /// <param name="resourceBuilder">The resource builder.</param> | ||
| /// <param name="projectPath">The absolute path to the project file.</param> | ||
| /// <param name="platformName">The platform name (e.g., "windows", "maccatalyst").</param> | ||
| /// <param name="platformDisplayName">The display name for the platform (e.g., "Windows", "Mac Catalyst").</param> | ||
| /// <param name="tfmExample">Example TFM for error messages (e.g., "net10.0-windows10.0.19041.0").</param> | ||
| /// <param name="isSupported">Function to check if the platform is supported on the current host.</param> | ||
| /// <param name="iconName">The icon name for the resource.</param> | ||
| /// <param name="additionalArgs">Optional additional command-line arguments to pass to dotnet run.</param> | ||
| internal static void ConfigurePlatformResource<T>( | ||
| IResourceBuilder<T> resourceBuilder, | ||
| string projectPath, | ||
| string platformName, | ||
| string platformDisplayName, | ||
| string tfmExample, | ||
| Func<bool> isSupported, | ||
| string iconName = "Desktop", | ||
| params string[] additionalArgs) where T : ProjectResource | ||
| { | ||
| // Check if the project has the platform TFM and get the actual TFM value | ||
| var platformTfm = ProjectFileReader.GetPlatformTargetFramework(projectPath, platformName); | ||
|
|
||
| // Set the command line arguments with the detected TFM if available | ||
| resourceBuilder.WithArgs(context => | ||
| { | ||
| context.Args.Add("run"); | ||
| if (!string.IsNullOrEmpty(platformTfm)) | ||
| { | ||
| context.Args.Add("-f"); | ||
| context.Args.Add(platformTfm); | ||
| } | ||
| // Add any additional platform-specific arguments | ||
| foreach (var arg in additionalArgs) | ||
| { | ||
| context.Args.Add(arg); | ||
| } | ||
| }); | ||
|
|
||
| resourceBuilder | ||
| .WithOtlpExporter() | ||
| .WithIconName(iconName) | ||
| .WithExplicitStart(); | ||
|
|
||
| // Validate the platform TFM when the resource is about to start | ||
| resourceBuilder.OnBeforeResourceStarted((resource, eventing, ct) => | ||
| { | ||
| // If we couldn't detect the TFM earlier, fail the resource start | ||
| if (string.IsNullOrEmpty(platformTfm)) | ||
| { | ||
| throw new DistributedApplicationException( | ||
| $"Unable to detect {platformDisplayName} target framework in project '{projectPath}'. " + | ||
| $"Ensure the project file contains a TargetFramework or TargetFrameworks element with a {platformDisplayName} target framework (e.g., {tfmExample}) " + | ||
| $"or remove the Add{platformDisplayName.Replace(" ", "")}Device() call from your AppHost."); | ||
| } | ||
|
|
||
| return Task.CompletedTask; | ||
| }); | ||
|
|
||
| // Check if platform is supported on the current host | ||
| if (!isSupported()) | ||
| { | ||
| var reason = $"{platformDisplayName} platform not available on this host"; | ||
|
|
||
| // Mark as unsupported | ||
| resourceBuilder.WithAnnotation(new UnsupportedPlatformAnnotation(reason), ResourceAnnotationMutationBehavior.Append); | ||
|
|
||
| // Add an event subscriber to set the "Unsupported" state after orchestrator initialization | ||
| var appBuilder = resourceBuilder.ApplicationBuilder; | ||
| appBuilder.Services.TryAddEventingSubscriber<UnsupportedPlatformEventSubscriber>(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think make this derive from
IResourceor maybe evenIResourceWithParent.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in commit ce2c2ef