diff --git a/examples/perl/CpanmApiIntegration.AppHost.TypeScript/package-lock.json b/examples/perl/CpanmApiIntegration.AppHost.TypeScript/package-lock.json
index fb7360f42..1ce03f7a8 100644
--- a/examples/perl/CpanmApiIntegration.AppHost.TypeScript/package-lock.json
+++ b/examples/perl/CpanmApiIntegration.AppHost.TypeScript/package-lock.json
@@ -761,6 +761,7 @@
"integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.72.0",
"@typescript-eslint/types": "8.72.0",
@@ -952,6 +953,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1202,6 +1204,7 @@
"integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"workspaces": [
"packages/*"
],
@@ -1589,6 +1592,7 @@
"integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
@@ -1938,6 +1942,7 @@
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -2019,6 +2024,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/Annotations/PerlModuleInstallerAnnotation.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/Annotations/PerlModuleInstallerAnnotation.cs
index 7a8603d0a..d0911a469 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Perl/Annotations/PerlModuleInstallerAnnotation.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Perl/Annotations/PerlModuleInstallerAnnotation.cs
@@ -7,7 +7,13 @@ namespace CommunityToolkit.Aspire.Hosting.Perl.Annotations;
/// This annotation tracks the that performs the installation.
///
/// The child executable resource that runs the install command.
-internal sealed class PerlModuleInstallerAnnotation(ExecutableResource installerResource) : IResourceAnnotation
+/// The original Perl module name requested for installation.
+internal sealed class PerlModuleInstallerAnnotation(ExecutableResource installerResource, string packageName) : IResourceAnnotation
{
public ExecutableResource Resource { get; } = installerResource;
+
+ ///
+ /// Gets the original, case-sensitive Perl module identity.
+ ///
+ public string PackageName { get; } = packageName;
}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs
index e08f12534..e037fa263 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs
@@ -215,6 +215,10 @@ public static IResourceBuilder WithCarton(
/// existing installer and its completion dependency. The installer retains the
/// and values from the first call.
///
+ ///
+ /// In run mode, the generated installer resource name is already in use by a different
+ /// module or resource. Module identities are compared using ordinal, case-sensitive matching.
+ ///
[AspireExport]
public static IResourceBuilder WithPackage(
this IResourceBuilder builder,
@@ -226,6 +230,8 @@ public static IResourceBuilder WithPackage(
ArgumentException.ThrowIfNullOrEmpty(packageName);
EnsurePackageInstallIsSupported(builder);
+ AddPackageInstaller(builder, packageName, force, skipTest);
+
builder.WithAnnotation(new PerlRequiredModuleAnnotation
{
Name = packageName,
@@ -233,8 +239,6 @@ public static IResourceBuilder WithPackage(
SkipTest = skipTest
});
- AddPackageInstaller(builder, packageName, force, skipTest);
-
return builder;
}
@@ -396,8 +400,21 @@ private static void AddPackageInstaller(
if (existingResource is not null)
{
- // Installer already exists for this package
- return;
+ var existingInstaller = resource.Resource.Annotations
+ .OfType()
+ .SingleOrDefault(annotation => ReferenceEquals(annotation.Resource, existingResource.Resource));
+
+ if (existingInstaller is not null &&
+ string.Equals(existingInstaller.PackageName, packageName, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ throw new InvalidOperationException(
+ $"Cannot install Perl module '{packageName}': installer resource name '{existingResource.Resource.Name}' is already used " +
+ (existingInstaller is not null
+ ? $"by module '{existingInstaller.PackageName}'."
+ : "by another resource."));
}
var installer = new PerlModuleInstallerResource(
@@ -451,13 +468,13 @@ private static void AddPackageInstaller(
// Make the parent resource wait for the installer to complete
resource.WaitForCompletion(installerBuilder);
- resource.WithAnnotation(new PerlModuleInstallerAnnotation(installer));
+ resource.WithAnnotation(new PerlModuleInstallerAnnotation(installer, packageName));
// Eagerly propagate local::lib env vars if the annotation already exists
TryAttachLocalLibToInstallerResource(installer, resource.Resource);
// Eagerly propagate certificate trust env vars if configured
- TryAttachCertificateTrustToInstallerResource(installer, resource.Resource);
+ TryAttachCertificateTrustToInstallerResource(installer, resource);
}
///
@@ -538,7 +555,7 @@ private static void AddProjectDependencyInstaller(
TryAttachLocalLibEnvironmentToProjectInstaller(resource);
// Eagerly propagate certificate trust env vars if configured
- TryAttachCertificateTrustToInstallerResource(installer, resource.Resource);
+ TryAttachCertificateTrustToInstallerResource(installer, resource);
}
///
@@ -626,13 +643,14 @@ private static void TryAttachLocalLibToExistingInstallers(
/// Uses on the parent as a guard —
/// only propagates when cert trust is enabled. Skips if already attached.
///
+ /// The type of the parent Perl application resource.
/// The installer resource that receives certificate variables.
- /// The parent Perl application resource with certificate trust configuration.
- private static void TryAttachCertificateTrustToInstallerResource(
+ /// The parent Perl resource builder with certificate trust configuration.
+ private static void TryAttachCertificateTrustToInstallerResource(
ExecutableResource installerResource,
- PerlAppResource parentResource)
+ IResourceBuilder resource) where TResource : PerlAppResource
{
- if (!parentResource.Annotations.OfType().Any())
+ if (!resource.Resource.Annotations.OfType().Any())
{
return;
}
@@ -644,20 +662,9 @@ private static void TryAttachCertificateTrustToInstallerResource(
installerResource.Annotations.Add(new PerlCertificateTrustAnnotation());
- installerResource.Annotations.Add(new EnvironmentCallbackAnnotation(context =>
- {
- // Propagate by reading from the process environment — Aspire sets these
- // on the host process when certificate trust is configured.
- var sslCertFile = Environment.GetEnvironmentVariable("SSL_CERT_FILE");
- if (sslCertFile is not null)
- {
- context.EnvironmentVariables["SSL_CERT_FILE"] = sslCertFile;
- context.EnvironmentVariables["PERL_LWP_SSL_CA_FILE"] = sslCertFile;
- context.EnvironmentVariables["MOJO_CA_FILE"] = sslCertFile;
- }
-
- return Task.CompletedTask;
- }));
+ // Unlike Python installers, Perl installers retain certificate trust for CPAN HTTPS requests.
+ resource.ApplicationBuilder.CreateResourceBuilder(installerResource)
+ .WithCertificateTrustConfiguration(ConfigurePerlCertificateTrust);
}
///
@@ -672,12 +679,12 @@ private static void TryAttachCertificateTrustToExistingInstallers(
{
foreach (var moduleAnnotation in resource.Resource.Annotations.OfType())
{
- TryAttachCertificateTrustToInstallerResource(moduleAnnotation.Resource, resource.Resource);
+ TryAttachCertificateTrustToInstallerResource(moduleAnnotation.Resource, resource);
}
if (resource.Resource.TryGetLastAnnotation(out var projectAnnotation))
{
- TryAttachCertificateTrustToInstallerResource(projectAnnotation.Resource, resource.Resource);
+ TryAttachCertificateTrustToInstallerResource(projectAnnotation.Resource, resource);
}
}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs
index a08c1b129..bb42e040d 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs
@@ -497,6 +497,7 @@ public static IResourceBuilder WithLocalLib(
/// variables that common Perl HTTP libraries respect.
/// Sets SSL_CERT_FILE (IO::Socket::SSL / LWP), PERL_LWP_SSL_CA_FILE (LWP::UserAgent),
/// and MOJO_CA_FILE (Mojolicious) to the certificate bundle path provided by Aspire.
+ /// Also configures package and project dependency installers using each resource's own certificate trust context.
///
/// The type of the Perl application resource.
/// The resource builder.
@@ -515,17 +516,7 @@ public static IResourceBuilder WithPerlCertificateTrust(
builder.Resource.Annotations.Add(new PerlCertificateTrustAnnotation());
- builder.WithCertificateTrustConfiguration(context =>
- {
- if (context.CertificateBundlePath is { } bundlePath)
- {
- context.EnvironmentVariables["SSL_CERT_FILE"] = bundlePath;
- context.EnvironmentVariables["PERL_LWP_SSL_CA_FILE"] = bundlePath;
- context.EnvironmentVariables["MOJO_CA_FILE"] = bundlePath;
- }
-
- return Task.CompletedTask;
- });
+ builder.WithCertificateTrustConfiguration(ConfigurePerlCertificateTrust);
// Propagate cert trust env vars to any existing installer child resources
TryAttachCertificateTrustToExistingInstallers(builder);
@@ -533,6 +524,21 @@ public static IResourceBuilder WithPerlCertificateTrust(
return builder;
}
+ ///
+ /// Configures Perl SSL/TLS environment variables from the resource's certificate bundle.
+ ///
+ private static Task ConfigurePerlCertificateTrust(CertificateTrustConfigurationCallbackAnnotationContext context)
+ {
+ if (context.CertificateBundlePath is { } bundlePath)
+ {
+ context.EnvironmentVariables["SSL_CERT_FILE"] = bundlePath;
+ context.EnvironmentVariables["PERL_LWP_SSL_CA_FILE"] = bundlePath;
+ context.EnvironmentVariables["MOJO_CA_FILE"] = bundlePath;
+ }
+
+ return Task.CompletedTask;
+ }
+
///
/// Resolves the local::lib path from the resource's ,
/// returning null when no local::lib is configured.
diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/README.md b/src/CommunityToolkit.Aspire.Hosting.Perl/README.md
index 2d5d19a4a..7bf1a5166 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Perl/README.md
+++ b/src/CommunityToolkit.Aspire.Hosting.Perl/README.md
@@ -146,6 +146,26 @@ In run mode, calling `WithPackage()` repeatedly for the same module on the same
reuses one installer and one completion dependency, including for names such as `Foo::Bar`.
The installer keeps the `force` and `skipTest` values from the first call.
+Module identities are case-sensitive. If different modules map to the same installer resource
+name, `WithPackage()` throws `InvalidOperationException` instead of silently skipping a module.
+For example, `Foo88Bar` and `Foo::Bar` both map to `app-Foo88Bar-installer` on a resource named
+`app`. Case-only differences also collide because Aspire resource names are case-insensitive.
+An installer belonging to another resource is never reused. These checks apply only in run mode;
+publish mode retains the module requests without creating local installers.
+
+---
+
+## Certificate trust
+
+The experimental `WithPerlCertificateTrust()` API (`CTASPIREPERL001`) configures
+`SSL_CERT_FILE`, `PERL_LWP_SSL_CA_FILE`, and `MOJO_CA_FILE` for the Perl app and its
+package and project dependency installers. Each resource uses the certificate bundle
+provided by its own Aspire certificate trust context, not the AppHost process's
+`SSL_CERT_FILE` environment variable.
+
+Call it before or after `WithPackage()` or `WithProjectDependencies()`; both orders
+are supported. Certificate trust remains opt-in.
+
---
## Additional Information
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/PublishModeWiringTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/PublishModeWiringTests.cs
index b3677c90a..752abc447 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/PublishModeWiringTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/PublishModeWiringTests.cs
@@ -1,6 +1,7 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.ApplicationModel.Docker;
+using CommunityToolkit.Aspire.Hosting.Perl.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace CommunityToolkit.Aspire.Hosting.Perl.Tests;
@@ -20,6 +21,27 @@ public void PublishMode_ResourceExistsInModel()
Assert.Contains(appModel.Resources, r => r.Name == "perl-app");
}
+ [Fact]
+ public void PublishMode_CollidingModuleNames_RetainsRequestsWithoutInstallers()
+ {
+ var builder = DistributedApplication.CreateBuilder(["--publisher", "manifest"]);
+ Assert.True(builder.ExecutionContext.IsPublishMode);
+
+ var resource = builder.AddPerlScript("app", "scripts", "app.pl")
+ .WithPackage("Foo88Bar")
+ .WithPackage("Foo::Bar");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ Assert.Empty(appModel.Resources.OfType());
+ Assert.Empty(resource.Resource.Annotations.OfType());
+ Assert.Collection(
+ resource.Resource.Annotations.OfType(),
+ annotation => Assert.Equal("Foo88Bar", annotation.Name),
+ annotation => Assert.Equal("Foo::Bar", annotation.Name));
+ }
+
[Fact]
public void RunMode_DoesNotAddDockerfileAnnotation()
{
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs
index 7602dcde8..b39ccf1ae 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs
@@ -5,6 +5,11 @@
namespace CommunityToolkit.Aspire.Hosting.Perl.Tests;
+// SSL_CERT_FILE is process-wide, so these tests must not overlap other collections.
+[CollectionDefinition(nameof(WithCertificateTrustTests), DisableParallelization = true)]
+public class WithCertificateTrustTestsCollection;
+
+[Collection(nameof(WithCertificateTrustTests))]
public class WithCertificateTrustTests
{
[Fact]
@@ -45,6 +50,9 @@ public void WithPerlCertificateTrust_CalledTwice_DoesNotDuplicateAnnotation()
#pragma warning disable CTASPIREPERL001
builder.AddPerlScript("perl-app", "scripts", "app.pl")
+ .WithCpanMinus()
+ .WithPackage("Mojolicious")
+ .WithProjectDependencies()
.WithPerlCertificateTrust()
.WithPerlCertificateTrust();
#pragma warning restore CTASPIREPERL001
@@ -52,60 +60,125 @@ public void WithPerlCertificateTrust_CalledTwice_DoesNotDuplicateAnnotation()
using var app = builder.Build();
var appModel = app.Services.GetRequiredService();
- var resource = Assert.Single(appModel.Resources.OfType());
-
- // Verify the marker annotation is present and singular
- Assert.Single(resource.Annotations.OfType());
-
- var annotations = resource.Annotations
- .OfType()
- .ToList();
- Assert.Single(annotations);
+ Assert.Single(appModel.Resources.OfType());
+ Assert.Equal(2, appModel.Resources.OfType().Count());
+
+ foreach (var resource in appModel.Resources)
+ {
+ Assert.Single(resource.Annotations.OfType());
+ Assert.Single(resource.Annotations.OfType());
+ }
}
[Fact]
- public void WithPerlCertificateTrust_PropagatesEnvVarsToInstallerChildResources()
+ public void Installers_WithoutPerlCertificateTrust_DoNotConfigureCertificateTrust()
{
var builder = DistributedApplication.CreateBuilder();
-
-#pragma warning disable CTASPIREPERL001
builder.AddPerlScript("perl-app", "scripts", "app.pl")
.WithCpanMinus()
.WithPackage("Mojolicious")
- .WithPerlCertificateTrust();
-#pragma warning restore CTASPIREPERL001
+ .WithProjectDependencies();
using var app = builder.Build();
var appModel = app.Services.GetRequiredService();
- var installerResource = Assert.Single(appModel.Resources.OfType());
-
- // Verify the cert trust marker annotation was propagated to the installer
- Assert.Single(installerResource.Annotations.OfType());
-
- // Verify exactly one env callback was added for cert trust
- Assert.Single(installerResource.Annotations.OfType());
+ var installers = appModel.Resources.OfType().ToList();
+ Assert.Equal(2, installers.Count);
+ Assert.All(installers, installer =>
+ {
+ Assert.Empty(installer.Annotations.OfType());
+ Assert.Empty(installer.Annotations.OfType());
+ });
}
- [Fact]
- public void WithPerlCertificateTrust_BeforeWithPackage_StillPropagates()
+ [Theory]
+ [InlineData(false, false, null)]
+ [InlineData(true, false, null)]
+ [InlineData(false, true, null)]
+ [InlineData(true, true, null)]
+ [InlineData(false, false, "shell-ca-bundle.pem")]
+ [InlineData(true, false, "shell-ca-bundle.pem")]
+ [InlineData(false, true, "shell-ca-bundle.pem")]
+ [InlineData(true, true, "shell-ca-bundle.pem")]
+ public async Task WithPerlCertificateTrust_InstallersUseTheirCertificateContext(
+ bool certificateTrustFirst,
+ bool projectDependencies,
+ string? processCertificateFile)
{
- var builder = DistributedApplication.CreateBuilder();
+ var originalCertificateFile = Environment.GetEnvironmentVariable("SSL_CERT_FILE");
+ try
+ {
+ Environment.SetEnvironmentVariable("SSL_CERT_FILE", processCertificateFile);
+ Assert.Equal(processCertificateFile, Environment.GetEnvironmentVariable("SSL_CERT_FILE"));
+
+ var builder = DistributedApplication.CreateBuilder();
+ var perlApp = builder.AddPerlScript("perl-app", "scripts", "app.pl")
+ .WithCpanMinus();
#pragma warning disable CTASPIREPERL001
- builder.AddPerlScript("perl-app", "scripts", "app.pl")
- .WithPerlCertificateTrust()
+ if (certificateTrustFirst)
+ {
+ perlApp.WithPerlCertificateTrust();
+ }
+
+ if (projectDependencies)
+ {
+ perlApp.WithProjectDependencies();
+ }
+ else
+ {
+ perlApp.WithPackage("Mojolicious");
+ }
+
+ if (!certificateTrustFirst)
+ {
+ perlApp.WithPerlCertificateTrust();
+ }
#pragma warning restore CTASPIREPERL001
- .WithCpanMinus()
- .WithPackage("Mojolicious");
-
- using var app = builder.Build();
-
- var appModel = app.Services.GetRequiredService();
- var installerResource = Assert.Single(appModel.Resources.OfType());
- // Verify the cert trust marker annotation was propagated even though
- // WithPerlCertificateTrust() was called before WithPackage()
- Assert.Single(installerResource.Annotations.OfType());
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var installerResource = Assert.Single(appModel.Resources.OfType());
+
+ foreach (var resource in new IResource[] { perlApp.Resource, installerResource })
+ {
+ var environment = new Dictionary();
+ var certificateDirectory = Path.Combine(builder.AppHostDirectory, resource.Name, "certs");
+ var bundlePath = ReferenceExpression.Create($"{Path.Combine(certificateDirectory, "bundle.pem")}");
+ var certificateContext = new CertificateTrustConfigurationCallbackAnnotationContext
+ {
+ ExecutionContext = builder.ExecutionContext,
+ Resource = resource,
+ Arguments = [],
+ EnvironmentVariables = environment,
+ CertificateBundlePath = bundlePath,
+ CertificateDirectoriesPath = ReferenceExpression.Create($"{certificateDirectory}"),
+ Scope = CertificateTrustScope.Override,
+ CancellationToken = TestContext.Current.CancellationToken
+ };
+
+ foreach (var callback in resource.Annotations.OfType())
+ {
+ await callback.Callback(certificateContext);
+ }
+
+ var environmentContext = new EnvironmentCallbackContext(builder.ExecutionContext, environment);
+ foreach (var callback in resource.Annotations.OfType())
+ {
+ await callback.Callback(environmentContext);
+ }
+
+ Assert.All(new[] { "SSL_CERT_FILE", "PERL_LWP_SSL_CA_FILE", "MOJO_CA_FILE" }, variable =>
+ {
+ Assert.True(environment.TryGetValue(variable, out var value), $"{resource.Name} is missing {variable}.");
+ Assert.Same(bundlePath, value);
+ });
+ }
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("SSL_CERT_FILE", originalCertificateFile);
+ }
}
}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithPackageTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithPackageTests.cs
index 319fac31b..88dab402c 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithPackageTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithPackageTests.cs
@@ -64,6 +64,92 @@ public void WithPackage_SameModuleTwice_AddsSingleWaitAnnotation(string moduleNa
Assert.Equal(WaitType.WaitForCompletion, waitAnnotation.WaitType);
}
+ [Theory]
+ [InlineData("Foo88Bar", "Foo::Bar", false)]
+ [InlineData("Foo::Bar", "Foo88Bar", false)]
+ [InlineData("Foo::Bar", "foo::bar", false)]
+ [InlineData("foo::bar", "Foo::Bar", false)]
+ [InlineData("Foo88Bar", "Foo::Bar", true)]
+ [InlineData("Foo::Bar", "Foo88Bar", true)]
+ [InlineData("Foo::Bar", "foo::bar", true)]
+ [InlineData("foo::bar", "Foo::Bar", true)]
+ public void WithPackage_DifferentModulesWithCollidingInstallerNames_Throws(
+ string firstModuleName,
+ string secondModuleName,
+ bool installerCommandChanged)
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var resource = builder.AddPerlScript("app", "scripts", "app.pl")
+ .WithPackage(firstModuleName);
+ var installer = Assert.Single(builder.Resources.OfType());
+
+ if (installerCommandChanged)
+ {
+ builder.CreateResourceBuilder(installer).WithCommand("cpanm");
+ }
+
+ var exception = Assert.Throws(() => resource.WithPackage(secondModuleName));
+
+ Assert.Contains(firstModuleName, exception.Message, StringComparison.Ordinal);
+ Assert.Contains(secondModuleName, exception.Message, StringComparison.Ordinal);
+ Assert.Contains(installer.Name, exception.Message, StringComparison.Ordinal);
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ Assert.Same(installer, Assert.Single(appModel.Resources.OfType()));
+ var requiredModule = Assert.Single(resource.Resource.Annotations.OfType());
+ Assert.Equal(firstModuleName, requiredModule.Name);
+ var installerAnnotation = Assert.Single(resource.Resource.Annotations.OfType());
+ Assert.Same(installer, installerAnnotation.Resource);
+ Assert.Equal(firstModuleName, installerAnnotation.PackageName);
+ var waitAnnotation = Assert.Single(resource.Resource.Annotations.OfType());
+ Assert.Same(installer, waitAnnotation.Resource);
+ Assert.Equal(WaitType.WaitForCompletion, waitAnnotation.WaitType);
+ }
+
+ [Theory]
+ [InlineData("Mojolicious")]
+ [InlineData("Foo::Bar")]
+ public void WithPackage_SameModuleAfterInstallerCommandChanges_ReusesInstaller(string moduleName)
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var resource = builder.AddPerlScript("app", "scripts", "app.pl")
+ .WithPackage(moduleName);
+ var installer = Assert.Single(builder.Resources.OfType());
+ builder.CreateResourceBuilder(installer).WithCommand("cpanm");
+
+ resource.WithPackage(moduleName);
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ Assert.Same(installer, Assert.Single(appModel.Resources.OfType()));
+ Assert.Equal("cpanm", installer.Command);
+ var installerAnnotation = Assert.Single(resource.Resource.Annotations.OfType());
+ Assert.Equal(moduleName, installerAnnotation.PackageName);
+ var waitAnnotation = Assert.Single(resource.Resource.Annotations.OfType());
+ Assert.Same(installer, waitAnnotation.Resource);
+ Assert.Equal(WaitType.WaitForCompletion, waitAnnotation.WaitType);
+ }
+
+ [Fact]
+ public void WithPackage_UnrelatedInstallerWithSameNameAndCommand_Throws()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var installer = new PerlModuleInstallerResource("app-Foo88Bar-installer", "Foo::Bar", builder.AppHostDirectory);
+ builder.AddResource(installer);
+ var resource = builder.AddPerlScript("app", "scripts", "app.pl");
+
+ var exception = Assert.Throws(() => resource.WithPackage("Foo::Bar"));
+
+ Assert.Contains("Foo::Bar", exception.Message, StringComparison.Ordinal);
+ Assert.Contains(installer.Name, exception.Message, StringComparison.Ordinal);
+ Assert.Empty(resource.Resource.Annotations.OfType());
+ Assert.Empty(resource.Resource.Annotations.OfType());
+ Assert.Empty(resource.Resource.Annotations.OfType());
+ }
+
[Theory]
[InlineData("DBI", false, false)]
[InlineData("DBI", true, false)]