diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 1e382a42..9f506378 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -83,77 +83,55 @@ namespace Microsoft.Tye var projectServices = services.Where(s => !string.IsNullOrEmpty(s.Project)); var projectMetadata = new Dictionary(); - using (var directory = TempDirectory.Create()) - { - var projectPath = Path.Combine(directory.DirectoryPath, Path.GetRandomFileName() + ".proj"); + var msbuildEvaluationResult = await EvaluateProjectsAsync( + projects: projectServices, + configRoot: config.Source.DirectoryName!, + output: output); + var msbuildEvaluationOutput = msbuildEvaluationResult + .StandardOutput + .Split(Environment.NewLine); - var sb = new StringBuilder(); - sb.AppendLine(""); - sb.AppendLine(" "); + var multiTFMProjects = new List(); - foreach (var project in projectServices) + foreach (var line in msbuildEvaluationOutput) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith("Microsoft.Tye metadata: ")) { - var expandedProject = Environment.ExpandEnvironmentVariables(project.Project!); - project.ProjectFullPath = Path.Combine(config.Source.DirectoryName!, expandedProject); - - if (!File.Exists(project.ProjectFullPath)) - { - throw new CommandException($"Failed to locate project: '{project.ProjectFullPath}'."); - } + var values = line.Split(':', 3); + var projectName = values[1].Trim(); + var metadataPath = values[2].Trim(); + projectMetadata.Add(projectName, metadataPath); - sb.AppendLine($" $"{kvp.Name}={kvp.Value}").Aggregate((a, b) => a + ";" + b) : string.Empty)}" + - $"{(string.IsNullOrEmpty(framework) ? string.Empty : $";TargetFramework={framework}")}" + - $"\" />"); + output.WriteDebugLine($"Resolved metadata for service {projectName} at {metadataPath}"); } - sb.AppendLine(@" "); - - sb.AppendLine($@" "); - sb.AppendLine($@" "); - - sb.AppendLine(" "); - sb.AppendLine(""); - File.WriteAllText(projectPath, sb.ToString()); - - output.WriteDebugLine("Restoring and evaluating projects"); - - var msbuildEvaluationResult = await ProcessUtil.RunAsync( - "dotnet", - $"build " + - $"\"{projectPath}\" " + - // CustomAfterMicrosoftCommonTargets is imported by non-crosstargeting (single TFM) projects - $"/p:CustomAfterMicrosoftCommonTargets={Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ProjectEvaluation.targets")} " + - // CustomAfterMicrosoftCommonCrossTargetingTargets is imported by crosstargeting (multi-TFM) projects - // This ensures projects properties are evaluated correctly. However, multi-TFM projects must specify - // a specific TFM to build/run/publish and will otherwise throw an exception. - $"/p:CustomAfterMicrosoftCommonCrossTargetingTargets={Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ProjectEvaluation.targets")} " + - $"/nologo", - throwOnError: false, - workingDirectory: directory.DirectoryPath); - - // If the build fails, we're not really blocked from doing our work. - // For now we just log the output to debug. There are errors that occur during - // running these targets we don't really care as long as we get the data. - if (msbuildEvaluationResult.ExitCode != 0) + else if (trimmed.StartsWith("Microsoft.Tye cross-targeting project: ")) { - output.WriteDebugLine($"Evaluating project failed with exit code {msbuildEvaluationResult.ExitCode}:" + - $"{Environment.NewLine}Ouptut: {msbuildEvaluationResult.StandardOutput}" + - $"{Environment.NewLine}Error: {msbuildEvaluationResult.StandardError}"); + var values = line.Split(':', 2); + var projectName = values[1].Trim(); + + var multiTFMConfigService = projectServices.First(p => string.Equals(p.Name, projectName, StringComparison.OrdinalIgnoreCase)); + multiTFMConfigService.BuildProperties.Add(new BuildProperty { Name = "TargetFramework", Value = framework ?? string.Empty }); + multiTFMProjects.Add(multiTFMConfigService); } + } + + if (multiTFMProjects.Any()) + { + output.WriteDebugLine("Re-evaluating multi-targeted projects"); - var msbuildEvaluationOutput = msbuildEvaluationResult + var multiTFMEvaluationResult = await EvaluateProjectsAsync( + projects: multiTFMProjects, + configRoot: config.Source.DirectoryName!, + output: output); + var multiTFMEvaluationOutput = multiTFMEvaluationResult .StandardOutput .Split(Environment.NewLine); - foreach (var line in msbuildEvaluationOutput) + foreach (var line in multiTFMEvaluationOutput) { - if (line.Trim().StartsWith("Microsoft.Tye metadata: ")) + var trimmed = line.Trim(); + if (trimmed.StartsWith("Microsoft.Tye metadata: ")) { var values = line.Split(':', 3); var projectName = values[1].Trim(); @@ -162,11 +140,17 @@ namespace Microsoft.Tye output.WriteDebugLine($"Resolved metadata for service {projectName} at {metadataPath}"); } + else if (trimmed.StartsWith("Microsoft.Tye cross-targeting project: ")) + { + var values = line.Split(':', 2); + var projectName = values[1].Trim(); + throw new CommandException($"Unable to run {projectName}. Your project targets multiple frameworks. Specify which framework to run using '--framework' or a build property in tye.yaml."); + } } - - output.WriteDebugLine($"Restore and project evaluation took: {sw.Elapsed.TotalMilliseconds}ms"); } + output.WriteDebugLine($"Restore and project evaluation took: {sw.Elapsed.TotalMilliseconds}ms"); + foreach (var configService in services) { ServiceBuilder service; @@ -191,7 +175,6 @@ namespace Microsoft.Tye } project.Replicas = configService.Replicas ?? 1; - project.Liveness = configService.Liveness != null ? GetProbeBuilder(configService.Liveness) : null; project.Readiness = configService.Readiness != null ? GetProbeBuilder(configService.Readiness) : null; @@ -207,12 +190,6 @@ namespace Microsoft.Tye ProjectReader.ReadProjectDetails(output, project, projectMetadata[configService.Name]); - if (framework != null && project.TargetFrameworks.Any()) - { - // Only use the TargetFramework for the "--framework" if it's a multi-targeted project and an override is provided - project.BuildProperties["TargetFramework"] = framework; - } - // Do k8s by default. project.ManifestInfo = new KubernetesManifestInfo(); } @@ -498,6 +475,73 @@ namespace Microsoft.Tye return root; } + private static async Task EvaluateProjectsAsync(IEnumerable projects, string configRoot, OutputContext output) + { + using var directory = TempDirectory.Create(); + var projectPath = Path.Combine(directory.DirectoryPath, Path.GetRandomFileName() + ".proj"); + + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(" "); + + foreach (var project in projects) + { + var expandedProject = Environment.ExpandEnvironmentVariables(project.Project!); + project.ProjectFullPath = Path.Combine(configRoot, expandedProject); + + if (!File.Exists(project.ProjectFullPath)) + { + throw new CommandException($"Failed to locate project: '{project.ProjectFullPath}'."); + } + + sb.AppendLine($" $"{kvp.Name}={kvp.Value}").Aggregate((a, b) => a + ";" + b) : string.Empty)}" + + $"\" />"); + } + sb.AppendLine(@" "); + + sb.AppendLine($@" "); + sb.AppendLine($@" "); + + sb.AppendLine(" "); + sb.AppendLine(""); + File.WriteAllText(projectPath, sb.ToString()); + + output.WriteDebugLine("Restoring and evaluating projects"); + + var msbuildEvaluationResult = await ProcessUtil.RunAsync( + "dotnet", + $"build " + + $"\"{projectPath}\" " + + // CustomAfterMicrosoftCommonTargets is imported by non-crosstargeting (single TFM) projects + $"/p:CustomAfterMicrosoftCommonTargets={Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ProjectEvaluation.targets")} " + + // CustomAfterMicrosoftCommonCrossTargetingTargets is imported by crosstargeting (multi-TFM) projects + // This ensures projects properties are evaluated correctly. However, multi-TFM projects must specify + // a specific TFM to build/run/publish and will otherwise throw an exception. + $"/p:CustomAfterMicrosoftCommonCrossTargetingTargets={Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ProjectEvaluation.targets")} " + + $"/nologo", + throwOnError: false, + workingDirectory: directory.DirectoryPath); + + // If the build fails, we're not really blocked from doing our work. + // For now we just log the output to debug. There are errors that occur during + // running these targets we don't really care as long as we get the data. + if (msbuildEvaluationResult.ExitCode != 0) + { + output.WriteDebugLine($"Evaluating project failed with exit code {msbuildEvaluationResult.ExitCode}:" + + $"{Environment.NewLine}Ouptut: {msbuildEvaluationResult.StandardOutput}" + + $"{Environment.NewLine}Error: {msbuildEvaluationResult.StandardError}"); + } + + return msbuildEvaluationResult; + } + private static string? GetDockerFileContext(FileInfo source, ConfigService configService) { if (configService.DockerFileContext == null) diff --git a/src/Microsoft.Tye.Core/StandardOptions.cs b/src/Microsoft.Tye.Core/StandardOptions.cs index 783dafd5..fbed4d93 100644 --- a/src/Microsoft.Tye.Core/StandardOptions.cs +++ b/src/Microsoft.Tye.Core/StandardOptions.cs @@ -49,9 +49,9 @@ namespace Microsoft.Tye public static Option Framework => new Option(new string[] { "-f", "--framework" }) { - Description = "The target framework override to use for all cross-targeting projects with multiple TFMs. " + + Description = "The target framework hint to use for all cross-targeting projects with multiple TFMs. " + "This value must be a valid target framework for each individual cross-targeting project. " + - "Non-crosstargeting projects will ignore this value. ", + "Non-crosstargeting projects will ignore this hint and the value TFM configured in tye.yaml will override this hint. ", Argument = new Argument("framework") { Arity = ArgumentArity.ExactlyOne diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index fdd835ff..706cfadc 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -121,11 +121,11 @@ namespace Microsoft.Tye.Hosting var projectPath = Path.Combine(directory.DirectoryPath, Path.GetRandomFileName() + ".proj"); var sb = new StringBuilder(); - sb.AppendLine(@" - "); + sb.AppendLine(@""); foreach (var group in projectGroups) { + sb.AppendLine(@" "); foreach (var p in group.Value.Services) { sb.AppendLine($" <{group.Value.GroupName} Include=\"{p.Status.ProjectFilePath}\" />"); diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index e8840f52..2e79c9dc 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -121,21 +121,6 @@ namespace Microsoft.Tye } else if (service is DotnetProjectServiceBuilder project) { - if (project.TargetFrameworks.Length > 1) - { - if (project.BuildProperties.TryGetValue("TargetFramework", out var targetFramework)) - { - if (!project.TargetFrameworks.Contains(targetFramework)) - { - throw new InvalidOperationException($"Unable to run {project.Name}. The specified TargetFramework is not one of the existing TargetFrameworks in the project."); - } - } - else - { - throw new InvalidOperationException($"Unable to run {project.Name}. Your project targets multiple frameworks. Specify which framework to run using '--framework'."); - } - } - if (project.RunCommand == null) { throw new InvalidOperationException($"Unable to run {project.Name}. The project does not have a run command"); diff --git a/src/tye/ProjectEvaluation.targets b/src/tye/ProjectEvaluation.targets index 03d12b51..40882b08 100644 --- a/src/tye/ProjectEvaluation.targets +++ b/src/tye/ProjectEvaluation.targets @@ -1,48 +1,50 @@  + + + Restore;ResolveReferences;ResolvePackageDependenciesDesignTime;PrepareResources;PrepareResources;GetAssemblyAttributes + + Restore + + + - - Restore;ResolveReferences;ResolvePackageDependenciesDesignTime;PrepareResources;PrepareResources;GetAssemblyAttributes - - Restore + <_MicrosoftTye_MetadataFile>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)MicrosoftTye.ProjectMetadata.txt')) + <_MicrosoftTye_ProjectFrameworkReference>@(FrameworkReference, '%3B') + <_MicrosoftTye_ProjectFrameworks>$(TargetFrameworks.Replace(';', '%3B')) - - - <_MicrosoftTye_MetadataFile>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)MicrosoftTye.ProjectMetadata.txt')) - <_MicrosoftTye_ProjectFrameworkReference>@(FrameworkReference, '%3B') - <_MicrosoftTye_ProjectFrameworks>$(TargetFrameworks.Replace(';', '%3B')) - - - - <_MicrosoftTye_ProjectMetadata Include="AssemblyInformationalVersion: $(AssemblyInformationalVersion)" /> - <_MicrosoftTye_ProjectMetadata Include="InformationalVersion: $(InformationalVersion)" /> - <_MicrosoftTye_ProjectMetadata Include="Version: $(Version)" /> - <_MicrosoftTye_ProjectMetadata Include="TargetFrameworks: $(_MicrosoftTye_ProjectFrameworks)" /> - <_MicrosoftTye_ProjectMetadata Include="RunCommand: $(RunCommand)" /> - <_MicrosoftTye_ProjectMetadata Include="RunArguments: $(RunArguments)" /> - <_MicrosoftTye_ProjectMetadata Include="TargetPath: $(TargetPath)" /> - <_MicrosoftTye_ProjectMetadata Include="PublishDir: $(PublishDir)" /> - <_MicrosoftTye_ProjectMetadata Include="AssemblyName: $(AssemblyName)" /> - <_MicrosoftTye_ProjectMetadata Include="IntermediateOutputPath: $(IntermediateOutputPath)" /> - <_MicrosoftTye_ProjectMetadata Include="TargetFramework: $(TargetFramework)" /> - <_MicrosoftTye_ProjectMetadata Include="_ShortFrameworkIdentifier: $(_ShortFrameworkIdentifier)" /> - <_MicrosoftTye_ProjectMetadata Include="_ShortFrameworkVersion: $(_ShortFrameworkVersion)" /> - <_MicrosoftTye_ProjectMetadata Include="_TargetFrameworkVersionWithoutV: $(_TargetFrameworkVersionWithoutV)" /> - <_MicrosoftTye_ProjectMetadata Include="FrameworkReference: $(_MicrosoftTye_ProjectFrameworkReference)" /> - <_MicrosoftTye_ProjectMetadata Include="ContainerBaseImage: $(ContainerBaseImage)" /> - <_MicrosoftTye_ProjectMetadata Include="ContainerBaseTag: $(ContainerBaseTag)" /> - <_MicrosoftTye_ProjectMetadata Include="MicrosoftNETPlatformLibrary: $(MicrosoftNETPlatformLibrary)" /> - <_MicrosoftTye_ProjectMetadata Include="_AspNetCoreAppSharedFxIsEnabled: $(_AspNetCoreAppSharedFxIsEnabled)" /> - <_MicrosoftTye_ProjectMetadata Include="UsingMicrosoftNETSdkWeb: $(UsingMicrosoftNETSdkWeb)" /> - + + <_MicrosoftTye_ProjectMetadata Include="AssemblyInformationalVersion: $(AssemblyInformationalVersion)" /> + <_MicrosoftTye_ProjectMetadata Include="InformationalVersion: $(InformationalVersion)" /> + <_MicrosoftTye_ProjectMetadata Include="Version: $(Version)" /> + <_MicrosoftTye_ProjectMetadata Include="TargetFrameworks: $(_MicrosoftTye_ProjectFrameworks)" /> + <_MicrosoftTye_ProjectMetadata Include="RunCommand: $(RunCommand)" /> + <_MicrosoftTye_ProjectMetadata Include="RunArguments: $(RunArguments)" /> + <_MicrosoftTye_ProjectMetadata Include="TargetPath: $(TargetPath)" /> + <_MicrosoftTye_ProjectMetadata Include="PublishDir: $(PublishDir)" /> + <_MicrosoftTye_ProjectMetadata Include="AssemblyName: $(AssemblyName)" /> + <_MicrosoftTye_ProjectMetadata Include="IntermediateOutputPath: $(IntermediateOutputPath)" /> + <_MicrosoftTye_ProjectMetadata Include="TargetFramework: $(TargetFramework)" /> + <_MicrosoftTye_ProjectMetadata Include="_ShortFrameworkIdentifier: $(_ShortFrameworkIdentifier)" /> + <_MicrosoftTye_ProjectMetadata Include="_ShortFrameworkVersion: $(_ShortFrameworkVersion)" /> + <_MicrosoftTye_ProjectMetadata Include="_TargetFrameworkVersionWithoutV: $(_TargetFrameworkVersionWithoutV)" /> + <_MicrosoftTye_ProjectMetadata Include="FrameworkReference: $(_MicrosoftTye_ProjectFrameworkReference)" /> + <_MicrosoftTye_ProjectMetadata Include="ContainerBaseImage: $(ContainerBaseImage)" /> + <_MicrosoftTye_ProjectMetadata Include="ContainerBaseTag: $(ContainerBaseTag)" /> + <_MicrosoftTye_ProjectMetadata Include="MicrosoftNETPlatformLibrary: $(MicrosoftNETPlatformLibrary)" /> + <_MicrosoftTye_ProjectMetadata Include="_AspNetCoreAppSharedFxIsEnabled: $(_AspNetCoreAppSharedFxIsEnabled)" /> + <_MicrosoftTye_ProjectMetadata Include="UsingMicrosoftNETSdkWeb: $(UsingMicrosoftNETSdkWeb)" /> + - + - + + - + \ No newline at end of file diff --git a/test/E2ETest/ApplicationFactoryTests.cs b/test/E2ETest/ApplicationFactoryTests.cs index b31ae4af..3646d258 100644 --- a/test/E2ETest/ApplicationFactoryTests.cs +++ b/test/E2ETest/ApplicationFactoryTests.cs @@ -146,7 +146,7 @@ services: } [Fact] - public async Task TargetFrameworkFromCliArgsOverwriteYaml() + public async Task TargetFrameworkFromCliArgsDoesNotOverwriteYaml() { using var projectDirectory = TestHelpers.CopyTestProjectDirectory(Path.Combine("multi-targetframeworks")); var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye-with-netcoreapp21.yaml"); @@ -161,7 +161,7 @@ services: var containsTargetFramework = ((DotnetProjectServiceBuilder)service).BuildProperties.TryGetValue("TargetFramework", out var targetFramework); Assert.True(containsTargetFramework); - Assert.Equal("netcoreapp3.1", targetFramework); + Assert.Equal("netcoreapp2.1", targetFramework); } [Fact] @@ -191,22 +191,28 @@ services: // Debug targets can be null if not specified, so make sure calling host.Start does not throw. var outputContext = new OutputContext(_sink, Verbosity.Debug); var projectFile = new FileInfo(yamlFile); - var applicationBuilder = await ApplicationFactory.CreateAsync(outputContext, projectFile); - Assert.Single(applicationBuilder.Services); - var service = applicationBuilder.Services.Single(s => s.Name == "multi-targetframeworks"); + await Assert.ThrowsAsync(async () => await ApplicationFactory.CreateAsync(outputContext, projectFile)); + } - var containsTargetFramework = ((DotnetProjectServiceBuilder)service).BuildProperties.TryGetValue("TargetFramework", out var targetFramework); - Assert.False(containsTargetFramework); + [Fact] + public async Task ThrowIfSpecifyTargetFrameworkNotDefinedIsCsproj() + { + using var projectDirectory = TestHelpers.CopyTestProjectDirectory(Path.Combine("multi-targetframeworks")); + var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye-no-buildproperties.yaml"); - Assert.Throws(() => applicationBuilder.ToHostingApplication()); + // Debug targets can be null if not specified, so make sure calling host.Start does not throw. + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var projectFile = new FileInfo(yamlFile); + + await Assert.ThrowsAsync(async () => await ApplicationFactory.CreateAsync(outputContext, projectFile, "net5.0")); } [Fact] - public async Task ThrowIfSpecifyTargetFrameworkNotDefinedIsCsproj() + public async Task ThrowIfSpecifyTargetFrameworkIsInvalid() { using var projectDirectory = TestHelpers.CopyTestProjectDirectory(Path.Combine("multi-targetframeworks")); - var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye-with-netcoreapp21.yaml"); + var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye-no-buildproperties.yaml"); // Debug targets can be null if not specified, so make sure calling host.Start does not throw. var outputContext = new OutputContext(_sink, Verbosity.Debug); diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index fda69e83..c8a4dd2f 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -1005,13 +1005,13 @@ services: [ConditionalFact] [SkipIfDockerNotRunning] - public async Task RunCliArgOverrideYamlMultipleTargetFrameworksTest() + public async Task RunCliArgDoesNotOverrideYamlMultipleTargetFrameworksTest() { using var projectDirectory = CopyTestProjectDirectory("multi-targetframeworks"); - var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-with-netcoreapp21.yaml")); + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-with-netcoreapp31.yaml")); var outputContext = new OutputContext(_sink, Verbosity.Debug); - var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1"); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp2.1"); var handler = new HttpClientHandler {