|
// 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.Eventing;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
namespace Aspire.Hosting.Maui.Utilities;
/// <summary>
/// Annotation that enables iOS environment variable support via MSBuild targets file.
/// </summary>
/// <remarks>
/// iOS MAUI applications cannot receive environment variables directly through the process environment
/// when launched via `dotnet run`. Instead, environment variables must be passed through MSBuild properties.
/// This annotation marks a resource for processing by <see cref="MauiiOSEnvironmentSubscriber"/>.
/// </remarks>
internal sealed class MauiiOSEnvironmentAnnotation : IResourceAnnotation
{
// Marker annotation - actual logic is in the eventing subscriber
}
/// <summary>
/// Internal annotation to track that the callback for iOS environment variables has been registered.
/// </summary>
/// <remarks>
/// This is a marker annotation used to prevent duplicate callback registration.
/// The actual file path is managed within the callback closure and doesn't need to be stored here.
/// </remarks>
internal sealed class MauiiOSEnvironmentProcessedAnnotation : IResourceAnnotation
{
}
/// <summary>
/// Event subscriber that processes <see cref="MauiiOSEnvironmentAnnotation"/> annotations.
/// </summary>
internal sealed class MauiiOSEnvironmentSubscriber(
DistributedApplicationExecutionContext executionContext,
ResourceLoggerService loggerService,
ResourceNotificationService notificationService) : IDistributedApplicationEventingSubscriber
{
public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext execContext, CancellationToken cancellationToken)
{
eventing.Subscribe<BeforeResourceStartedEvent>(OnBeforeResourceStartedAsync);
return Task.CompletedTask;
}
private async Task OnBeforeResourceStartedAsync(BeforeResourceStartedEvent @event, CancellationToken cancellationToken)
{
var resource = @event.Resource;
// Only process iOS resources with the environment annotation
if (resource is not (MauiiOSDeviceResource or MauiiOSSimulatorResource))
{
return;
}
if (!resource.TryGetLastAnnotation<MauiiOSEnvironmentAnnotation>(out _))
{
return;
}
var logger = loggerService.GetLogger(resource);
// Check if we've already added the callback
if (resource.TryGetLastAnnotation<MauiiOSEnvironmentProcessedAnnotation>(out _))
{
// Already processed - callback is already registered
return;
}
try
{
// Add a CommandLineArgsCallback that will generate the targets file
// This runs AFTER all environment callbacks have been processed
// The callback itself ensures idempotency by only generating the file once
string? generatedFilePath = null;
resource.Annotations.Add(new CommandLineArgsCallbackAnnotation(async context =>
{
// Only generate the file once, even if this callback is invoked multiple times
if (generatedFilePath is null)
{
generatedFilePath = await MauiEnvironmentHelper.CreateiOSEnvironmentTargetsFileAsync(
resource,
executionContext,
logger,
cancellationToken
).ConfigureAwait(false);
if (generatedFilePath is not null)
{
logger.LogInformation("Generated environment targets file for iOS: {Path}", generatedFilePath);
}
}
if (generatedFilePath is not null)
{
// Add the targets file as an MSBuild property via command-line argument
var commandLineArg = $"-p:CustomAfterMicrosoftCommonTargets={generatedFilePath}";
context.Args.Add(commandLineArg);
}
}));
// Mark as processed to avoid duplicate callbacks
resource.Annotations.Add(new MauiiOSEnvironmentProcessedAnnotation());
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to configure iOS environment variables");
// Report the error through the notification service
await notificationService.PublishUpdateAsync(resource, s => s with
{
State = new ResourceStateSnapshot("Failed to configure environment", KnownResourceStateStyles.Error)
}).ConfigureAwait(false);
throw;
}
}
}
|