public static Task ReadProjectDetailsAsync(OutputContext output, DotnetProjectServiceBuilder project) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } EnsureMSBuildRegistered(output, project.ProjectFile); EvaluateProject(output, project); if (!SemVersion.TryParse(project.Version, out var version)) { output.WriteInfoLine($"No version or invalid version '{project.Version}' found, using default."); version = new SemVersion(0, 1, 0); project.Version = version.ToString(); } return(Task.CompletedTask); }
public static void ReadProjectDetails(OutputContext output, DotnetProjectServiceBuilder project, string metadataFile) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (project is null) { throw new ArgumentNullException(nameof(metadataFile)); } EvaluateProject(output, project, metadataFile); if (!SemVersion.TryParse(project.Version, out var version)) { output.WriteInfoLine($"No version or invalid version '{project.Version}' found, using default."); version = new SemVersion(0, 1, 0); project.Version = version.ToString(); } }
public static void ApplyContainerDefaults(ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container) { if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (string.IsNullOrEmpty(container.BaseImageName) && project.IsAspNet) { container.BaseImageName = "mcr.microsoft.com/dotnet/core/aspnet"; } else if (string.IsNullOrEmpty(container.BaseImageName)) { container.BaseImageName = "mcr.microsoft.com/dotnet/core/runtime"; } if (string.IsNullOrEmpty(container.BaseImageTag) && (project.TargetFrameworkName == "netcoreapp" || project.TargetFrameworkName == "net")) { container.BaseImageTag = project.TargetFrameworkVersion; } if (string.IsNullOrEmpty(container.BaseImageTag)) { throw new CommandException($"Unsupported TFM {project.TargetFramework}."); } container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk"; container.BuildImageTag ??= project.TargetFrameworkVersion; if (container.ImageName == null && application.Registry?.Hostname == null) { container.ImageName ??= project.Name.ToLowerInvariant(); } else if (container.ImageName == null && application.Registry?.Hostname != null) { container.ImageName ??= $"{application.Registry?.Hostname}/{project.Name.ToLowerInvariant()}"; } container.ImageTag ??= project.Version?.Replace("+", "-") ?? "latest"; // Disable color in the logs project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("DOTNET_LOGGING__CONSOLE__DISABLECOLORS") { Value = "true" }); }
public static void ApplyContainerDefaults(ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container) { if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (string.IsNullOrEmpty(container.BaseImageName) && project.IsAspNet) { container.BaseImageName = "mcr.microsoft.com/dotnet/core/aspnet"; } else if (string.IsNullOrEmpty(container.BaseImageName)) { container.BaseImageName = "mcr.microsoft.com/dotnet/core/runtime"; } if (string.IsNullOrEmpty(container.BaseImageTag) && project.TargetFrameworkName == "netcoreapp") { container.BaseImageTag = project.TargetFrameworkVersion; } if (string.IsNullOrEmpty(container.BaseImageTag)) { throw new CommandException($"Unsupported TFM {project.TargetFramework}."); } container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk"; container.BuildImageTag ??= "3.1"; if (container.ImageName == null && application.Registry?.Hostname == null) { container.ImageName ??= project.Name.ToLowerInvariant(); } else if (container.ImageName == null && application.Registry?.Hostname != null) { container.ImageName ??= $"{application.Registry?.Hostname}/{project.Name.ToLowerInvariant()}"; } container.ImageTag ??= project.Version?.Replace("+", "-") ?? "latest"; }
public static SidecarBuilder GetOrAddSidecar(DotnetProjectServiceBuilder project) { // Bring your rain boots. project.RelocateDiagnosticsDomainSockets = true; var sidecar = project.Sidecars.FirstOrDefault(s => s.Name == "tye-diag-agent"); if (sidecar is object) { return(sidecar); } sidecar = new SidecarBuilder("tye-diag-agent", "rynowak/tye-diag-agent", "0.1") { Args = { "--kubernetes=true", $"--service={project.Name}", $"--assemblyName={project.AssemblyName}", }, }; project.Sidecars.Add(sidecar); return(sidecar); }
public static async Task WriteDockerfileAsync(OutputContext output, ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container, string filePath) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (filePath is null) { throw new ArgumentNullException(nameof(filePath)); } await using var stream = File.OpenWrite(filePath); await using var writer = new StreamWriter(stream, encoding: new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: -1, leaveOpen: true); var entryPoint = project.AssemblyName; output.WriteDebugLine($"Writing Dockerfile to '{filePath}'."); if (container.UseMultiphaseDockerfile ?? true) { await WriteMultiphaseDockerfileAsync(writer, entryPoint, container); } else { await WriteLocalPublishDockerfileAsync(writer, entryPoint, container); } output.WriteDebugLine("Done writing Dockerfile."); }
private static void EvaluateProject(OutputContext output, DotnetProjectServiceBuilder project, string metadataFile) { var sw = Stopwatch.StartNew(); var metadata = new Dictionary <string, string>(); var metadataKVPs = File.ReadLines(metadataFile).Select(l => l.Split(new[] { ':' }, 2)); foreach (var metadataKVP in metadataKVPs) { if (!string.IsNullOrEmpty(metadataKVP[1])) { metadata.Add(metadataKVP[0], metadataKVP[1].Trim()); } } // Reading a few different version properties to be more resilient. var version = GetMetadataValueOrNull("AssemblyInformationalVersion") ?? GetMetadataValueOrNull("InformationalVersion") ?? GetMetadataValueOrEmpty("Version"); project.Version = version; output.WriteDebugLine($"Found application version: {version}"); project.TargetFrameworks = GetMetadataValueOrNull("TargetFrameworks")?.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty <string>(); // Figure out if functions app. // If so, run app with function host. project.RunCommand = GetMetadataValueOrEmpty("RunCommand"); project.RunArguments = GetMetadataValueOrEmpty("RunArguments"); project.TargetPath = GetMetadataValueOrEmpty("TargetPath"); project.PublishDir = GetMetadataValueOrEmpty("PublishDir"); project.AssemblyName = GetMetadataValueOrEmpty("AssemblyName"); project.IntermediateOutputPath = GetMetadataValueOrEmpty("IntermediateOutputPath"); output.WriteDebugLine($"RunCommand={project.RunCommand}"); output.WriteDebugLine($"RunArguments={project.RunArguments}"); output.WriteDebugLine($"TargetPath={project.TargetPath}"); output.WriteDebugLine($"PublishDir={project.PublishDir}"); output.WriteDebugLine($"AssemblyName={project.AssemblyName}"); output.WriteDebugLine($"IntermediateOutputPath={project.IntermediateOutputPath}"); // Normalize directories to their absolute paths project.IntermediateOutputPath = Path.Combine(project.ProjectFile.DirectoryName !, NormalizePath(project.IntermediateOutputPath)); project.TargetPath = Path.Combine(project.ProjectFile.DirectoryName !, NormalizePath(project.TargetPath)); project.PublishDir = Path.Combine(project.ProjectFile.DirectoryName !, NormalizePath(project.PublishDir)); var targetFramework = GetMetadataValueOrEmpty("TargetFramework"); project.TargetFramework = targetFramework; output.WriteDebugLine($"Found target framework: {targetFramework}"); // TODO: Parse the name and version manually out of the TargetFramework field if it's non-null project.TargetFrameworkName = GetMetadataValueOrNull("_ShortFrameworkIdentifier") ?? project.TargetFramework.TrimEnd(".0123456789".ToCharArray()); project.TargetFrameworkVersion = GetMetadataValueOrNull("_ShortFrameworkVersion") ?? GetMetadataValueOrEmpty("_TargetFrameworkVersionWithoutV"); output.WriteDebugLine($"Parsed target framework name: {project.TargetFrameworkName}"); output.WriteDebugLine($"Parsed target framework version: {project.TargetFrameworkVersion}"); var sharedFrameworks = GetMetadataValueOrNull("FrameworkReference")?.Split(';') ?? Enumerable.Empty <string>(); project.Frameworks.AddRange(sharedFrameworks.Select(s => new Framework(s))); output.WriteDebugLine($"Found shared frameworks: {string.Join(", ", sharedFrameworks)}"); // determine container base image if (project.ContainerInfo != null) { project.ContainerInfo.BaseImageName = GetMetadataValueOrEmpty("ContainerBaseImage"); project.ContainerInfo.BaseImageTag = GetMetadataValueOrEmpty("ContainerBaseTag"); } project.IsAspNet = project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App") || GetMetadataValueOrEmpty("MicrosoftNETPlatformLibrary") == "Microsoft.AspNetCore.App" || MetadataIsTrue("_AspNetCoreAppSharedFxIsEnabled") || MetadataIsTrue("UsingMicrosoftNETSdkWeb"); output.WriteDebugLine($"IsAspNet={project.IsAspNet}"); output.WriteDebugLine($"Evaluation Took: {sw.Elapsed.TotalMilliseconds}ms"); string?GetMetadataValueOrNull(string key) => metadata !.TryGetValue(key, out var value) ? value : null; string GetMetadataValueOrEmpty(string key) => metadata !.TryGetValue(key, out var value) ? value : string.Empty; bool MetadataIsTrue(string key) => metadata !.TryGetValue(key, out var value) && bool.Parse(value); }
private static void EvaluateProject(OutputContext output, DotnetProjectServiceBuilder project) { var sw = Stopwatch.StartNew(); // Currently we only log at debug level. var logger = new ConsoleLogger( verbosity: LoggerVerbosity.Normal, write: message => output.WriteDebug(message), colorSet: null, colorReset: null); // We need to isolate projects from each other for testing. MSBuild does not support // loading the same project twice in the same collection. var projectCollection = new ProjectCollection(); ProjectInstance projectInstance; Microsoft.Build.Evaluation.Project msbuildProject; try { output.WriteDebugLine($"Loading project '{project.ProjectFile.FullName}'."); msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(project.ProjectFile.FullName, new ProjectOptions() { ProjectCollection = projectCollection, GlobalProperties = project.BuildProperties }); projectInstance = msbuildProject.CreateProjectInstance(); output.WriteDebugLine($"Loaded project '{project.ProjectFile.FullName}'."); } catch (Exception ex) { throw new CommandException($"Failed to load project: '{project.ProjectFile.FullName}'.", ex); } try { AssemblyLoadContext.Default.Resolving += ResolveAssembly; output.WriteDebugLine($"Restoring project '{project.ProjectFile.FullName}'."); // Similar to what MSBuild does for restore: // https://github.com/microsoft/msbuild/blob/3453beee039fb6f5ccc54ac783ebeced31fec472/src/MSBuild/XMake.cs#L1417 // // We need to do restore as a separate operation var restoreRequest = new BuildRequestData( projectInstance, targetsToBuild: new[] { "Restore" }, hostServices: null, flags: BuildRequestDataFlags.ClearCachesAfterBuild | BuildRequestDataFlags.SkipNonexistentTargets | BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports); var parameters = new BuildParameters(projectCollection) { Loggers = new[] { logger, }, }; // We don't really look at the result, because it's not clear we should halt totally // if restore fails. var restoreResult = BuildManager.DefaultBuildManager.Build(parameters, restoreRequest); output.WriteDebugLine($"Restored project '{project.ProjectFile.FullName}'."); msbuildProject.MarkDirty(); projectInstance = msbuildProject.CreateProjectInstance(); var targets = new List <string>() { "ResolveReferences", "ResolvePackageDependenciesDesignTime", "PrepareResources", "GetAssemblyAttributes", }; var result = projectInstance.Build( targets: targets.ToArray(), loggers: new[] { logger, }); // 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. } finally { AssemblyLoadContext.Default.Resolving -= ResolveAssembly; } // Reading a few different version properties to be more resilient. var version = projectInstance.GetProperty("AssemblyInformationalVersion")?.EvaluatedValue ?? projectInstance.GetProperty("InformationalVersion")?.EvaluatedValue ?? projectInstance.GetProperty("Version").EvaluatedValue; project.Version = version; output.WriteDebugLine($"Found application version: {version}"); var targetFrameworks = projectInstance.GetPropertyValue("TargetFrameworks"); project.TargetFrameworks = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty <string>(); // Figure out if functions app. // If so, run app with function host. project.RunCommand = projectInstance.GetPropertyValue("RunCommand"); project.RunArguments = projectInstance.GetPropertyValue("RunArguments"); project.TargetPath = projectInstance.GetPropertyValue("TargetPath"); project.PublishDir = projectInstance.GetPropertyValue("PublishDir"); project.AssemblyName = projectInstance.GetPropertyValue("AssemblyName"); project.IntermediateOutputPath = projectInstance.GetPropertyValue("IntermediateOutputPath"); output.WriteDebugLine($"RunCommand={project.RunCommand}"); output.WriteDebugLine($"RunArguments={project.RunArguments}"); output.WriteDebugLine($"TargetPath={project.TargetPath}"); output.WriteDebugLine($"PublishDir={project.PublishDir}"); output.WriteDebugLine($"AssemblyName={project.AssemblyName}"); output.WriteDebugLine($"IntermediateOutputPath={project.IntermediateOutputPath}"); // Normalize directories to their absolute paths project.IntermediateOutputPath = Path.Combine(project.ProjectFile.DirectoryName !, NormalizePath(project.IntermediateOutputPath)); project.TargetPath = Path.Combine(project.ProjectFile.DirectoryName !, NormalizePath(project.TargetPath)); project.PublishDir = Path.Combine(project.ProjectFile.DirectoryName !, NormalizePath(project.PublishDir)); var targetFramework = projectInstance.GetPropertyValue("TargetFramework"); project.TargetFramework = targetFramework; output.WriteDebugLine($"Found target framework: {targetFramework}"); // TODO: Parse the name and version manually out of the TargetFramework field if it's non-null project.TargetFrameworkName = projectInstance.GetPropertyValue("_ShortFrameworkIdentifier"); project.TargetFrameworkVersion = projectInstance.GetPropertyValue("_ShortFrameworkVersion") ?? projectInstance.GetPropertyValue("_TargetFrameworkVersionWithoutV"); var sharedFrameworks = projectInstance.GetItems("FrameworkReference").Select(i => i.EvaluatedInclude).ToList(); project.Frameworks.AddRange(sharedFrameworks.Select(s => new Framework(s))); output.WriteDebugLine($"Found shared frameworks: {string.Join(", ", sharedFrameworks)}"); // determine container base image if (project.ContainerInfo != null) { project.ContainerInfo.BaseImageName = projectInstance.GetPropertyValue("ContainerBaseImage"); project.ContainerInfo.BaseImageTag = projectInstance.GetPropertyValue("ContainerBaseTag"); } bool PropertyIsTrue(string property) { return(projectInstance.GetPropertyValue(property) is string s && !string.IsNullOrEmpty(s) && bool.Parse(s)); } project.IsAspNet = project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App") || projectInstance.GetPropertyValue("MicrosoftNETPlatformLibrary") == "Microsoft.AspNetCore.App" || PropertyIsTrue("_AspNetCoreAppSharedFxIsEnabled") || PropertyIsTrue("UsingMicrosoftNETSdkWeb"); output.WriteDebugLine($"IsAspNet={project.IsAspNet}"); output.WriteDebugLine($"Evaluation Took: {sw.Elapsed.TotalMilliseconds}ms"); // The Microsoft.Build.Locator doesn't handle the loading of other assemblies // that are shipped with MSBuild (ex NuGet). // // This means that the set of assemblies that need special handling depends on the targets // that we run :( // // This is workaround for this limitation based on the targets we need to run // to resolve references and versions. // // See: https://github.com/microsoft/MSBuildLocator/issues/86 Assembly?ResolveAssembly(AssemblyLoadContext context, AssemblyName assemblyName) { if (assemblyName.Name is object) { var msbuildDirectory = Environment.GetEnvironmentVariable("MSBuildExtensionsPath") !; var assemblyFilePath = Path.Combine(msbuildDirectory, assemblyName.Name + ".dll"); if (File.Exists(assemblyFilePath)) { return(context.LoadFromAssemblyPath(assemblyFilePath)); } } return(default);
public static async Task GenerateAsync(OutputContext output, ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (chart is null) { throw new ArgumentNullException(nameof(chart)); } if (outputDirectory is null) { throw new ArgumentNullException(nameof(outputDirectory)); } ApplyHelmChartDefaults(application, project, container, chart); // The directory with the charts needs to be the same as the chart name var chartDirectoryPath = Path.Combine(outputDirectory.FullName, chart.ChartName); Directory.CreateDirectory(chartDirectoryPath); var templateDirectoryPath = Path.Combine( Path.GetDirectoryName(typeof(HelmChartGenerator).Assembly.Location) !, "Templates", "Helm"); DirectoryCopy.Copy(templateDirectoryPath, chartDirectoryPath); // Write Chart.yaml // // apiVersion: v1 // name: <appname> // version: <version> // appVersion: <version> await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "Chart.yaml"), new[] { $"apiVersion: v1", $"name: {chart.ChartName}", $"# helm requires the version and appVersion to specified in Chart.yaml", $"# tye will override these values when packaging the chart", $"version: {project.Version.Replace('+', '-')}", $"appVersion: {project.Version.Replace('+', '-')}" }); // Write values.yaml // // image: // repository: rynowak.azurecr.io/rochambot/gamemaster await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "values.yaml"), new[] { $"image:", $" repository: {container.ImageName}", }); }
public static async Task BuildContainerImageAsync(OutputContext output, ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } string contextDirectory; var dockerFilePath = Path.Combine(project.ProjectFile.DirectoryName !, "Dockerfile"); TempFile? tempFile = null; TempDirectory?tempDirectory = null; try { // We need to know if this is a single-phase or multi-phase Dockerfile because the context directory will be // different depending on that choice. // // For the cases where generate a Dockerfile, we have the constraint that we need // to place it on the same drive (Windows) as the docker context. if (container.UseMultiphaseDockerfile ?? true) { // For a multi-phase Docker build, the context is always the project directory. contextDirectory = "."; if (File.Exists(dockerFilePath)) { output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'."); } else { // We need to write the file, let's stick it under obj. Directory.CreateDirectory(project.IntermediateOutputPath); dockerFilePath = Path.Combine(project.IntermediateOutputPath, "Dockerfile"); // Clean up file when done building image tempFile = new TempFile(dockerFilePath); await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, tempFile.FilePath); } } else { // For a single-phase Docker build the context is always the directory containing the publish // output. We need to put the Dockerfile in the context directory so it's on the same drive (Windows). var publishOutput = project.Outputs.OfType <ProjectPublishOutput>().FirstOrDefault(); if (publishOutput is null) { throw new InvalidOperationException("We should have published the project for a single-phase Dockerfile."); } contextDirectory = publishOutput.Directory.FullName; // Clean up directory when done building image tempDirectory = new TempDirectory(publishOutput.Directory); if (File.Exists(dockerFilePath)) { output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'."); File.Copy(dockerFilePath, Path.Combine(contextDirectory, "Dockerfile")); dockerFilePath = Path.Combine(contextDirectory, "Dockerfile"); } else { // No need to clean up, it's in a directory we're already cleaning up. dockerFilePath = Path.Combine(contextDirectory, "Dockerfile"); await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, dockerFilePath); } } output.WriteDebugLine("Running 'docker build'."); output.WriteCommandLine("docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\""); var capture = output.Capture(); var exitCode = await application.ContainerEngine.ExecuteAsync( $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"", project.ProjectFile.DirectoryName, stdOut : capture.StdOut, stdErr : capture.StdErr); output.WriteDebugLine($"Done running 'docker build' exit code: {exitCode}"); if (exitCode != 0) { throw new CommandException("'docker build' failed."); } output.WriteInfoLine($"Created Docker Image: '{container.ImageName}:{container.ImageTag}'"); project.Outputs.Add(new DockerImageOutput(container.ImageName !, container.ImageTag !)); } finally { tempDirectory?.Dispose(); tempFile?.Dispose(); } }
protected bool SkipWithoutDotnetProject(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out DotnetProjectServiceBuilder project) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (service is null) { throw new ArgumentNullException(nameof(service)); } if (service is DotnetProjectServiceBuilder p) { project = p; return(false); } output.WriteInfoLine($"Service '{service.Name}' does not have a project associated. Skipping."); project = default !;