diff --git a/cli/azd/internal/appdetect/dotnet_apphost.go b/cli/azd/internal/appdetect/dotnet_apphost.go index c6c5a5511a1..ba8e9456095 100644 --- a/cli/azd/internal/appdetect/dotnet_apphost.go +++ b/cli/azd/internal/appdetect/dotnet_apphost.go @@ -5,7 +5,6 @@ import ( "io/fs" "log" "path/filepath" - "strings" "github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet" ) @@ -26,7 +25,7 @@ func (ad *dotNetAppHostDetector) DetectProject(ctx context.Context, path string, switch ext { case ".csproj", ".fsproj", ".vbproj": projectPath := filepath.Join(path, name) - if isAppHost, err := ad.isAppHostProject(ctx, filepath.Join(projectPath)); err != nil { + if isAppHost, err := ad.dotnetCli.IsAspireHostProject(ctx, filepath.Join(projectPath)); err != nil { log.Printf("error checking if %s is an app host project: %v", projectPath, err) } else if isAppHost { return &Project{ @@ -40,14 +39,3 @@ func (ad *dotNetAppHostDetector) DetectProject(ctx context.Context, path string, return nil, nil } - -// isAppHostProject returns true if the project at the given path has an MS Build Property named "IsAspireHost" which is -// set to "true". -func (ad *dotNetAppHostDetector) isAppHostProject(ctx context.Context, projectPath string) (bool, error) { - value, err := ad.dotnetCli.GetMsBuildProperty(ctx, projectPath, "IsAspireHost") - if err != nil { - return false, err - } - - return strings.TrimSpace(value) == "true", nil -} diff --git a/cli/azd/internal/vsrpc/utils.go b/cli/azd/internal/vsrpc/utils.go index 6827ac339fb..c80dd3c2185 100644 --- a/cli/azd/internal/vsrpc/utils.go +++ b/cli/azd/internal/vsrpc/utils.go @@ -6,7 +6,6 @@ import ( "fmt" "log" "path/filepath" - "strings" "github.com/azure/azure-dev/cli/azd/pkg/apphost" "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" @@ -20,11 +19,11 @@ func appHostForProject( ) (*project.ServiceConfig, error) { for _, service := range pc.Services { if service.Language == project.ServiceLanguageDotNet { - isAppHost, err := dotnetCli.GetMsBuildProperty(ctx, service.Path(), "IsAspireHost") + isAppHost, err := dotnetCli.IsAspireHostProject(ctx, service.Path()) if err != nil { log.Printf("error checking if %s is an app host project: %v", service.Path(), err) } - if strings.TrimSpace(isAppHost) == "true" { + if isAppHost { return service, nil } } diff --git a/cli/azd/pkg/project/dotnet_importer.go b/cli/azd/pkg/project/dotnet_importer.go index 833d1c7f1c5..12a1ca96e86 100644 --- a/cli/azd/pkg/project/dotnet_importer.go +++ b/cli/azd/pkg/project/dotnet_importer.go @@ -83,7 +83,7 @@ func (ai *DotNetImporter) CanImport(ctx context.Context, projectPath string) (bo return v.is, v.err } - value, err := ai.dotnetCli.GetMsBuildProperty(ctx, projectPath, "IsAspireHost") + isAppHost, err := ai.dotnetCli.IsAspireHostProject(ctx, projectPath) if err != nil { ai.hostCheck[projectPath] = hostCheckResult{ is: false, @@ -94,11 +94,11 @@ func (ai *DotNetImporter) CanImport(ctx context.Context, projectPath string) (bo } ai.hostCheck[projectPath] = hostCheckResult{ - is: strings.TrimSpace(value) == "true", + is: isAppHost, err: nil, } - return strings.TrimSpace(value) == "true", nil + return isAppHost, nil } func (ai *DotNetImporter) ProjectInfrastructure(ctx context.Context, svcConfig *ServiceConfig) (*Infra, error) { diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go index fdfc039d96a..4f995630610 100644 --- a/cli/azd/pkg/project/importer_test.go +++ b/cli/azd/pkg/project/importer_test.go @@ -92,7 +92,7 @@ func TestImportManagerHasServiceErrorNoMultipleServicesWithAppHost(t *testing.T) slices.Contains(args.Args, "--getProperty:IsAspireHost") }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { return exec.RunResult{ - Stdout: "true", + Stdout: aspireAppHostSniffResult, ExitCode: 0, }, nil }) @@ -145,7 +145,7 @@ func TestImportManagerHasServiceErrorAppHostMustTargetContainerApp(t *testing.T) slices.Contains(args.Args, "--getProperty:IsAspireHost") }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { return exec.RunResult{ - Stdout: "true", + Stdout: aspireAppHostSniffResult, ExitCode: 0, }, nil }) @@ -278,7 +278,7 @@ func TestImportManagerProjectInfrastructureAspire(t *testing.T) { slices.Contains(args.Args, "--getProperty:IsAspireHost") }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { return exec.RunResult{ - Stdout: "true", + Stdout: aspireAppHostSniffResult, ExitCode: 0, }, nil }) @@ -462,3 +462,52 @@ func TestImportManager_SynthAllInfrastructure_FromResources(t *testing.T) { _, err = im.SynthAllInfrastructure(context.Background(), prjConfig) assert.Error(t, err) } + +// aspireAppHostSniffResult is mock data that would be returned by `dotnet msbuild` when fetching information about an +// Aspire project. This is used to simulate the scenario where a project is an Aspire project. A real Aspire project would +// have many entries in the ProjectCapability array (unrelated to the Aspire capability), but most have been omitted for +// simplicity. An unrelated entry is included to ensure we are looking at the entire array of capabilities. +// nolint: lll +var aspireAppHostSniffResult string = `{ + "Properties": { + "IsAspireHost": "true" + }, + "Items": { + "ProjectCapability": [ + { + "Identity": "LocalUserSecrets", + "FullPath": "/Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/LocalUserSecrets", + "RootDir": "/", + "Filename": "LocalUserSecrets", + "Extension": "", + "RelativeDir": "", + "Directory": "Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/", + "RecursiveDir": "", + "ModifiedTime": "", + "CreatedTime": "", + "AccessedTime": "", + "DefiningProjectFullPath": "/Users/matell/.nuget/packages/microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Configuration.UserSecrets.props", + "DefiningProjectDirectory": "/Users/matell/.nuget/packages/microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/", + "DefiningProjectName": "Microsoft.Extensions.Configuration.UserSecrets", + "DefiningProjectExtension": ".props" + }, + { + "Identity": "Aspire", + "FullPath": "/Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/Aspire", + "RootDir": "/", + "Filename": "Aspire", + "Extension": "", + "RelativeDir": "", + "Directory": "Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/", + "RecursiveDir": "", + "ModifiedTime": "", + "CreatedTime": "", + "AccessedTime": "", + "DefiningProjectFullPath": "/Users/matell/.nuget/packages/aspire.hosting.apphost/8.2.0/build/Aspire.Hosting.AppHost.targets", + "DefiningProjectDirectory": "/Users/matell/.nuget/packages/aspire.hosting.apphost/8.2.0/build/", + "DefiningProjectName": "Aspire.Hosting.AppHost", + "DefiningProjectExtension": ".targets" + } + ] + } +}` diff --git a/cli/azd/pkg/tools/dotnet/dotnet.go b/cli/azd/pkg/tools/dotnet/dotnet.go index a1e4be68ec8..5f4237e3055 100644 --- a/cli/azd/pkg/tools/dotnet/dotnet.go +++ b/cli/azd/pkg/tools/dotnet/dotnet.go @@ -303,6 +303,42 @@ func (cli *Cli) GetMsBuildProperty(ctx context.Context, project string, property return res.Stdout, nil } +// IsAspireHostProject returns true if the project at the given path has an MS Build Property named "IsAspireHost" which is +// set to true or has a ProjectCapability named "Aspire". +func (cli *Cli) IsAspireHostProject(ctx context.Context, projectPath string) (bool, error) { + runArgs := newDotNetRunArgs("msbuild", projectPath, "--getProperty:IsAspireHost", "--getItem:ProjectCapability") + res, err := cli.commandRunner.Run(ctx, runArgs) + if err != nil { + return false, fmt.Errorf("running dotnet msbuild on project '%s': %w", projectPath, err) + } + + var result struct { + Properties struct { + IsAspireHost string `json:"IsAspireHost"` + } `json:"Properties"` + Items struct { + ProjectCapability []struct { + Identity string `json:"Identity"` + } `json:"ProjectCapability"` + } `json:"Items"` + } + + if err := json.Unmarshal([]byte(res.Stdout), &result); err != nil { + return false, fmt.Errorf("unmarshal dotnet msbuild output: %w", err) + } + + hasAspireCapability := false + + for _, capability := range result.Items.ProjectCapability { + if capability.Identity == "Aspire" { + hasAspireCapability = true + break + } + } + + return result.Properties.IsAspireHost == "true" || hasAspireCapability, nil +} + func NewCli(commandRunner exec.CommandRunner) *Cli { return &Cli{ commandRunner: commandRunner,