|
// 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);
}
});
// Configure OTLP exporter with custom endpoint support
ConfigureOtlpExporter(resourceBuilder);
resourceBuilder
.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>();
}
}
/// <summary>
/// Configures OTLP exporter with support for Android-specific template replacement.
/// </summary>
/// <remarks>
/// <para>
/// For Android resources, we replace DCP template placeholders ({{...}}) with actual values
/// because Android environment files are generated before DCP's template replacement happens.
/// DCP normally replaces these templates when writing to the actual running process, but we
/// need the resolved values earlier for the MSBuild targets file.
/// </para>
/// <para>
/// This matches the pattern used by other non-DCP-launched resources like Docker Compose and
/// Azure App Service, which also manually set OTEL values instead of relying on DCP templates.
/// </para>
/// </remarks>
private static void ConfigureOtlpExporter<T>(IResourceBuilder<T> resourceBuilder) where T : ProjectResource
{
// Call the standard WithOtlpExporter which sets up all other OTLP configuration
resourceBuilder.WithOtlpExporter();
// For Android resources, replace DCP template placeholders that won't be resolved in time
var resource = resourceBuilder.Resource;
var instanceId = Guid.NewGuid().ToString(); // Generate unique instance ID
resourceBuilder.WithEnvironment(async context =>
{
await Task.CompletedTask.ConfigureAwait(false);
// Replace OTEL_SERVICE_NAME template with actual resource name
// DCP would normally set this to the resource name, so we do the same
if (context.EnvironmentVariables.TryGetValue("OTEL_SERVICE_NAME", out var serviceName))
{
if (serviceName is string serviceNameStr &&
serviceNameStr.Contains("{{", StringComparison.Ordinal) &&
serviceNameStr.Contains("}}", StringComparison.Ordinal))
{
context.EnvironmentVariables["OTEL_SERVICE_NAME"] = resource.Name;
}
}
// Replace OTEL_RESOURCE_ATTRIBUTES template with unique instance ID
// DCP would normally set this to a generated suffix, so we use a GUID
if (context.EnvironmentVariables.TryGetValue("OTEL_RESOURCE_ATTRIBUTES", out var resourceAttrs))
{
if (resourceAttrs is string resourceAttrsStr &&
resourceAttrsStr.Contains("{{", StringComparison.Ordinal) &&
resourceAttrsStr.Contains("}}", StringComparison.Ordinal))
{
context.EnvironmentVariables["OTEL_RESOURCE_ATTRIBUTES"] = $"service.instance.id={instanceId}";
}
}
});
}
}
|