diff --git a/src/BuildScriptGenerator/Constants.cs b/src/BuildScriptGenerator/Constants.cs index f8a2b9e1a5..5f65360db3 100644 --- a/src/BuildScriptGenerator/Constants.cs +++ b/src/BuildScriptGenerator/Constants.cs @@ -9,6 +9,6 @@ public static class Constants { public const string OryxEnvironmentSettingNamePrefix = "ORYX_"; public const string BuildEnvironmentFileName = "build.env"; - internal const string ManifestFileName = "oryx-manifest.toml"; + public const string ManifestFileName = "oryx-manifest.toml"; } } \ No newline at end of file diff --git a/src/BuildScriptGenerator/DotNetCore/DotnetCoreConstants.cs b/src/BuildScriptGenerator/DotNetCore/DotnetCoreConstants.cs index 25a710b14f..8da6355193 100644 --- a/src/BuildScriptGenerator/DotNetCore/DotnetCoreConstants.cs +++ b/src/BuildScriptGenerator/DotNetCore/DotnetCoreConstants.cs @@ -24,5 +24,6 @@ public static class DotnetCoreConstants public const string AspNetCoreAppPackageReference = "Microsoft.AspNetCore.App"; public const string ProjectFileLanguageDetectorProperty = "ProjectFile"; + public const string StartupFileName = "startupFileName"; } } \ No newline at end of file diff --git a/src/BuildScriptGenerator/DotNetCore/DotnetCorePlatform.cs b/src/BuildScriptGenerator/DotNetCore/DotnetCorePlatform.cs index f5cbd507dc..6d1e5e611d 100644 --- a/src/BuildScriptGenerator/DotNetCore/DotnetCorePlatform.cs +++ b/src/BuildScriptGenerator/DotNetCore/DotnetCorePlatform.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Xml.Linq; +using System.Xml.XPath; using Microsoft.Extensions.Logging; namespace Microsoft.Oryx.BuildScriptGenerator.DotNetCore @@ -42,21 +44,38 @@ public LanguageDetectorResult Detect(ISourceRepo sourceRepo) return _detector.Detect(sourceRepo); } - public BuildScriptSnippet GenerateBashBuildScriptSnippet(BuildScriptGeneratorContext scriptGeneratorContext) + public BuildScriptSnippet GenerateBashBuildScriptSnippet(BuildScriptGeneratorContext context) { - (string projectFile, string publishDir) = GetProjectFileAndPublishDir(scriptGeneratorContext.SourceRepo); + var buildProperties = new Dictionary(); + (string projectFile, string publishDir) = GetProjectFileAndPublishDir(context.SourceRepo); if (string.IsNullOrEmpty(projectFile) || string.IsNullOrEmpty(publishDir)) { return null; } + string startupFileName = null; + var projectFileContent = context.SourceRepo.ReadFile(projectFile); + var projFileDoc = XDocument.Load(new StringReader(projectFileContent)); + var assemblyNameElement = projFileDoc.XPathSelectElement("/Project/PropertyGroup/AssemblyName"); + if (assemblyNameElement == null) + { + var name = Path.GetFileNameWithoutExtension(projectFile); + startupFileName = $"{name}.dll"; + } + else + { + startupFileName = $"{assemblyNameElement.Value}.dll"; + } + + buildProperties[DotnetCoreConstants.StartupFileName] = startupFileName; + var props = new DotNetCoreBashBuildSnippetProperties { ProjectFile = projectFile, PublishDirectory = publishDir }; string script = TemplateHelpers.Render(TemplateHelpers.TemplateResource.DotNetCoreSnippet, props, _logger); - return new BuildScriptSnippet { BashBuildScriptSnippet = script }; + return new BuildScriptSnippet { BashBuildScriptSnippet = script, BuildProperties = buildProperties }; } public bool IsCleanRepo(ISourceRepo repo) diff --git a/src/startupscriptgenerator/common/buildManifestHelper.go b/src/startupscriptgenerator/common/buildManifestHelper.go new file mode 100644 index 0000000000..94ecdec360 --- /dev/null +++ b/src/startupscriptgenerator/common/buildManifestHelper.go @@ -0,0 +1,36 @@ +// -------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +// -------------------------------------------------------------------------------------------- + +package common + +import ( + "github.com/BurntSushi/toml" + "log" + "path/filepath" +) + +type BuildManifest struct { + StartupFileName string +} + +const ManifestFileName = "oryx-manifest.toml" + +func DeserializeBuildManifest(manifestFile string) BuildManifest { + var manifest BuildManifest + if _, err := toml.DecodeFile(manifestFile, &manifest); err != nil { + log.Fatal(err) + } + return manifest +} + +func GetBuildManifest(appPath string) BuildManifest { + buildManifest := BuildManifest{} + + tomlFile := filepath.Join(appPath, ManifestFileName) + if FileExists(tomlFile) { + buildManifest = DeserializeBuildManifest(tomlFile) + } + return buildManifest +} \ No newline at end of file diff --git a/src/startupscriptgenerator/dotnetcore/scriptgenerator.go b/src/startupscriptgenerator/dotnetcore/scriptgenerator.go index bbffadc453..8ed5605d90 100644 --- a/src/startupscriptgenerator/dotnetcore/scriptgenerator.go +++ b/src/startupscriptgenerator/dotnetcore/scriptgenerator.go @@ -15,11 +15,11 @@ import ( ) type DotnetCoreStartupScriptGenerator struct { - SourcePath string - AppPath string - UserStartupCommand string - DefaultAppFilePath string - BindPort string + SourcePath string + AppPath string + UserStartupCommand string + DefaultAppFilePath string + BindPort string } type projectDetails struct { @@ -51,10 +51,7 @@ type packageReference struct { const ProjectEnvironmentVariableName = "PROJECT" const DefaultBindPort = "8080" -var _retrievedProjectDetails = false -var _gotStartupFileName = false var _projDetails projectDetails = projectDetails{} -var _startupFileName = "" func (gen *DotnetCoreStartupScriptGenerator) GenerateEntrypointScript() string { logger := common.GetLogger("dotnetcore.scriptgenerator.GenerateEntrypointScript") @@ -124,11 +121,22 @@ func (gen *DotnetCoreStartupScriptGenerator) getStartupCommand() string { func (gen *DotnetCoreStartupScriptGenerator) getStartupDllFileName() string { logger := common.GetLogger("dotnetcore.scriptgenerator.getStartupDllFileName") - if _gotStartupFileName { - return _startupFileName + manifestFilePath := filepath.Join(gen.AppPath, common.ManifestFileName) + if common.FileExists(manifestFilePath) { + logger.LogInformation("Found build manifest file at '%s'.", manifestFilePath) + buildManifest := common.GetBuildManifest(gen.AppPath) + if buildManifest.StartupFileName == "" { + logger.LogInformation( + "Found build manifest file at '%s', but startup file name property is empty.", + manifestFilePath) + } else { + return buildManifest.StartupFileName + } + } else { + logger.LogInformation("Cound not find build manifest file at '%s'.", manifestFilePath) } - projDetails := gen.getProjectDetailsAndCache() + projDetails := gen.getProjectDetails() if projDetails.FullPath == "" { logger.LogError("Could not find the project file.") return "" @@ -149,19 +157,7 @@ func (gen *DotnetCoreStartupScriptGenerator) getStartupDllFileName() string { } else { startupFileName = assemblyName + ".dll" } - _startupFileName = startupFileName - _gotStartupFileName = true - return _startupFileName -} -func (gen *DotnetCoreStartupScriptGenerator) getProjectDetailsAndCache() projectDetails { - if _retrievedProjectDetails { - return _projDetails - } - - projDetails := gen.getProjectDetails() - _retrievedProjectDetails = true - _projDetails = projDetails - return _projDetails + return startupFileName } func (gen *DotnetCoreStartupScriptGenerator) getProjectDetails() projectDetails { diff --git a/tests/Oryx.BuildImage.Tests/DotNetCoreSampleAppsTest.cs b/tests/Oryx.BuildImage.Tests/DotNetCoreSampleAppsTest.cs index 60125c3012..f1f285a5ac 100644 --- a/tests/Oryx.BuildImage.Tests/DotNetCoreSampleAppsTest.cs +++ b/tests/Oryx.BuildImage.Tests/DotNetCoreSampleAppsTest.cs @@ -11,6 +11,7 @@ using Microsoft.Oryx.Tests.Common; using Xunit; using Xunit.Abstractions; +using ScriptGenerator=Microsoft.Oryx.BuildScriptGenerator; namespace Microsoft.Oryx.BuildImage.Tests { @@ -34,6 +35,10 @@ public void Builds_NetCore11App_UsingNetCore11_DotnetSdkVersion() var script = new ShellScriptBuilder() .AddBuildCommand($"{appDir} -o {appOutputDir}") .AddFileExistsCheck($"{appOutputDir}/{appName}.dll") + .AddFileExistsCheck($"{appOutputDir}/{ScriptGenerator.Constants.ManifestFileName}") + .AddStringExistsInFileCheck( + $"{DotnetCoreConstants.StartupFileName}=\"{appName}.dll\"", + $"{appOutputDir}/{ScriptGenerator.Constants.ManifestFileName}") .ToString(); // Act diff --git a/tests/Oryx.Integration.Tests/LocalDockerTests/DotNetCoreEndToEndTests.cs b/tests/Oryx.Integration.Tests/LocalDockerTests/DotNetCoreEndToEndTests.cs index 5715d66c3c..eaf028220f 100644 --- a/tests/Oryx.Integration.Tests/LocalDockerTests/DotNetCoreEndToEndTests.cs +++ b/tests/Oryx.Integration.Tests/LocalDockerTests/DotNetCoreEndToEndTests.cs @@ -5,9 +5,11 @@ using System.IO; using System.Threading.Tasks; +using Microsoft.Oryx.BuildScriptGenerator.DotNetCore; using Microsoft.Oryx.Tests.Common; using Xunit; using Xunit.Abstractions; +using ScriptGenerator = Microsoft.Oryx.BuildScriptGenerator; namespace Microsoft.Oryx.Integration.Tests.LocalDockerTests { @@ -89,6 +91,9 @@ public async Task CanBuildAndRun_NetCore11WebApp_HavingExplicitAssemblyName() var appOutputDir = $"{appDir}/myoutputdir"; var buildImageScript = new ShellScriptBuilder() .AddCommand($"oryx build {appDir} -l dotnet --language-version {dotnetcoreVersion} -o {appOutputDir}") + .AddStringExistsInFileCheck( + $"{DotnetCoreConstants.StartupFileName}=\"Yoyo.dll\"", + $"{appOutputDir}/{ScriptGenerator.Constants.ManifestFileName}") .ToString(); var runtimeImageScript = new ShellScriptBuilder() .AddCommand( @@ -223,7 +228,10 @@ public async Task CanBuildAndRun_NetCore21WebApp_HavingExplicitAssemblyName() var appOutputDir = $"{appDir}/myoutputdir"; var buildImageScript = new ShellScriptBuilder() .AddCommand($"oryx build {appDir} -l dotnet --language-version {dotnetcoreVersion} -o {appOutputDir}") - .ToString(); + .AddStringExistsInFileCheck( + $"{DotnetCoreConstants.StartupFileName}=\"Yoyo.dll\"", + $"{appOutputDir}/{ScriptGenerator.Constants.ManifestFileName}") + .ToString(); var runtimeImageScript = new ShellScriptBuilder() .AddCommand( $"oryx -appPath {appOutputDir} -sourcePath {appDir} -bindPort {ContainerPort}") @@ -501,6 +509,9 @@ public async Task CanBuildAndRun_NetCore21WebApp_HavingNestedProjectDirectory_Wh var buildImageScript = new ShellScriptBuilder() .AddCommand(setProjectEnvVariable) .AddCommand($"oryx build {repoDir} -o {appOutputDir}") // Do not specify language and version + .AddStringExistsInFileCheck( + $"{DotnetCoreConstants.StartupFileName}=\"WebApp1.dll\"", + $"{appOutputDir}/{ScriptGenerator.Constants.ManifestFileName}") .ToString(); var runtimeImageScript = new ShellScriptBuilder() .AddCommand(setProjectEnvVariable) @@ -535,7 +546,7 @@ await EndToEndTestHelper.BuildRunAndAssertAppAsync( } [Fact] - public async Task CanBuildAndRun_NetCore21WebApp_HavingNestedProjectDirectory() + public async Task CanBuildAndRun_NetCore21WebApp_HavingMultipleProjects() { // Arrange var appName = "MultiWebAppRepo"; @@ -549,6 +560,9 @@ public async Task CanBuildAndRun_NetCore21WebApp_HavingNestedProjectDirectory() var buildImageScript = new ShellScriptBuilder() .AddCommand(setProjectEnvVariable) .AddCommand($"oryx build {repoDir} -o {appOutputDir} -l dotnet --language-version {dotnetcoreVersion}") + .AddStringExistsInFileCheck( + $"{DotnetCoreConstants.StartupFileName}=\"WebApp1.dll\"", + $"{appOutputDir}/{ScriptGenerator.Constants.ManifestFileName}") .ToString(); var runtimeImageScript = new ShellScriptBuilder() .AddCommand(setProjectEnvVariable) diff --git a/tests/Oryx.RuntimeImage.Tests/DotnetCoreStartupScriptGenerationTest.cs b/tests/Oryx.RuntimeImage.Tests/DotnetCoreStartupScriptGenerationTest.cs index 8ce79f62bc..df799aadd1 100644 --- a/tests/Oryx.RuntimeImage.Tests/DotnetCoreStartupScriptGenerationTest.cs +++ b/tests/Oryx.RuntimeImage.Tests/DotnetCoreStartupScriptGenerationTest.cs @@ -22,7 +22,7 @@ public class DotnetCoreStartupScriptGenerationTest : TestBase @"" + ""; - private const string ProjectFileWithExplicitAssemblyName = + private const string ProjectFileWithExplicitAssemblyName = @"" + "netcoreapp2.1Foo" + @""; @@ -76,6 +76,123 @@ public void GeneratesScript_UsingProjectFileName() result.GetDebugInfo()); } + [Fact] + public void GeneratesScript_UsingStartupFileName_FromBuildManifest_NotUsingSourcePath() + { + // Arrange + var appDir = "/app"; + var appOutputDir = "/app/output"; + var expectedStartupCommand = $"dotnet \"different.dll\""; + var expectedWorkingDir = $"cd \"{appOutputDir}\""; + var script = new ShellScriptBuilder() + .AddCommand($"mkdir -p {appDir}") + .AddCommand($"echo '{RegularProjectFileContent}' > {appDir}/shoppingapp.csproj") + .AddCommand($"mkdir -p {appOutputDir}") + .AddCommand($"echo startupFileName=\\\"different.dll\\\" > {appOutputDir}/oryx-manifest.toml") + .AddCommand($"echo > {appOutputDir}/different.dll") + // NOTE: Do not specify source path argument + .AddCommand($"oryx -appPath {appOutputDir}") + .AddCommand($"cat {ScriptLocation}") + .ToString(); + + // Act + var result = _dockerCli.Run( + DotnetCoreRuntimeImageName, + commandToExecuteOnRun: "/bin/sh", + commandArguments: new[] + { + "-c", + script + }); + + // Assert + RunAsserts( + () => + { + Assert.True(result.IsSuccess); + Assert.Contains(expectedWorkingDir, result.StdOut); + Assert.Contains(expectedStartupCommand, result.StdOut); + }, + result.GetDebugInfo()); + } + + [Fact] + public void GeneratesScript_UsingSourcePath_IfStartupFileNameIsEmpty_FromSourcePath() + { + // Arrange + var appDir = "/app"; + var appOutputDir = "/app/output"; + var expectedStartupCommand = $"dotnet \"shoppingapp.dll\""; + var expectedWorkingDir = $"cd \"{appOutputDir}\""; + var script = new ShellScriptBuilder() + .AddCommand($"mkdir -p {appDir}") + .AddCommand($"echo '{RegularProjectFileContent}' > {appDir}/shoppingapp.csproj") + .AddCommand($"mkdir -p {appOutputDir}") + .AddCommand($"echo startupFileName=\\\"\\\" > {appOutputDir}/oryx-manifest.toml") + .AddCommand($"echo > {appOutputDir}/shoppingapp.dll") + .AddCommand($"oryx -appPath {appOutputDir} -sourcePath {appDir}") + .AddCommand($"cat {ScriptLocation}") + .ToString(); + + // Act + var result = _dockerCli.Run( + DotnetCoreRuntimeImageName, + commandToExecuteOnRun: "/bin/sh", + commandArguments: new[] + { + "-c", + script + }); + + // Assert + RunAsserts( + () => + { + Assert.True(result.IsSuccess); + Assert.Contains(expectedWorkingDir, result.StdOut); + Assert.Contains(expectedStartupCommand, result.StdOut); + }, + result.GetDebugInfo()); + } + + [Fact] + public void GeneratesScript_WithDefaultAppFilePath_IfStartupFileFromBuildManifest_DoesNotExist() + { + // Arrange + var appDir = "/app"; + var outputDir = "/app/output"; + var defaultWebAppFile = "/tmp/defaultwebapp.dll"; + var expectedStartupCommand = $"dotnet \"{defaultWebAppFile}\""; + var script = new ShellScriptBuilder() + .AddCommand($"mkdir -p {appDir}") + .AddCommand($"echo '{RegularProjectFileContent}' > {appDir}/shoppingapp.csproj") + .AddCommand($"mkdir -p {outputDir}") + .AddCommand($"echo startupFileName=\\\"doesnotexist.dll\\\" > {outputDir}/oryx-manifest.toml") + .AddCommand($"echo > /tmp/defaultwebapp.dll") + .AddCommand($"oryx -appPath {outputDir} -defaultAppFilePath {defaultWebAppFile}") + .AddCommand($"cat {ScriptLocation}") + .ToString(); + + // Act + var result = _dockerCli.Run( + DotnetCoreRuntimeImageName, + commandToExecuteOnRun: "/bin/sh", + commandArguments: new[] + { + "-c", + script + }); + + // Assert + RunAsserts( + () => + { + Assert.True(result.IsSuccess); + Assert.Contains(expectedStartupCommand, result.StdOut); + }, + result.GetDebugInfo()); + } + [Fact] public void GeneratesScript_UsingExplicitAssemblyName() { @@ -463,7 +580,7 @@ public void GeneratedScript_CanRunStartupScriptsFromAppRoot() // Act var res = _dockerCli.Run("oryxdevms/dotnetcore-2.2", "/bin/sh", new[] { "-c", script }); - + // Assert RunAsserts(() => Assert.Equal(res.ExitCode, exitCodeSentinel), res.GetDebugInfo()); }