using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Diagnostics; using System.Runtime.InteropServices; using Amazon.Common.DotNetCli.Tools; using System.Linq; namespace Amazon.Lambda.Tools { /// /// Wrapper around the dotnet cli used to execute the publish command. /// public class LambdaDotNetCLIWrapper { string _workingDirectory; IToolLogger _logger; public LambdaDotNetCLIWrapper(IToolLogger logger, string workingDirectory) { this._logger = logger; this._workingDirectory = workingDirectory; } /// /// Execute the dotnet store command on the provided package manifest /// /// /// /// /// /// /// /// public int Store(LambdaToolsDefaults defaults, string projectLocation, string outputLocation, string targetFramework, string packageManifest, string architecture, bool enableOptimization) { if (outputLocation == null) throw new ArgumentNullException(nameof(outputLocation)); if (Directory.Exists(outputLocation)) { try { Directory.Delete(outputLocation, true); _logger?.WriteLine("Deleted previous publish folder"); } catch (Exception e) { _logger?.WriteLine($"Warning unable to delete previous publish folder: {e.Message}"); } } var dotnetCLI = FindExecutableInPath("dotnet.exe"); if (dotnetCLI == null) dotnetCLI = FindExecutableInPath("dotnet"); if (string.IsNullOrEmpty(dotnetCLI)) throw new Exception("Failed to locate dotnet CLI executable. Make sure the dotnet CLI is installed in the environment PATH."); var fullProjectLocation = this._workingDirectory; if (!string.IsNullOrEmpty(projectLocation)) { fullProjectLocation = Utilities.DetermineProjectLocation(this._workingDirectory, projectLocation); } var fullPackageManifest = Path.Combine(fullProjectLocation, packageManifest); _logger?.WriteLine($"... invoking 'dotnet store' for manifest {fullPackageManifest} into output directory {outputLocation}"); StringBuilder arguments = new StringBuilder("store"); if (!string.IsNullOrEmpty(outputLocation)) { arguments.Append($" --output \"{outputLocation}\""); } if (!string.IsNullOrEmpty(targetFramework)) { arguments.Append($" --framework \"{targetFramework}\""); } arguments.Append($" --manifest \"{fullPackageManifest}\""); arguments.Append($" --runtime {LambdaUtilities.DetermineRuntimeParameter(targetFramework, architecture)}"); if(!enableOptimization) { arguments.Append(" --skip-optimization"); } var psi = new ProcessStartInfo { FileName = dotnetCLI, Arguments = arguments.ToString(), WorkingDirectory = this._workingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; var handler = (DataReceivedEventHandler)((o, e) => { if (string.IsNullOrEmpty(e.Data)) return; // Skip outputting this warning message as it adds a lot of noise to the output and is not actionable. // Full warning message being skipped: message NETSDK1062: // Unable to use package assets cache due to I/O error. This can occur when the same project is built // more than once in parallel. Performance may be degraded, but the build result will not be impacted. if (e.Data.Contains("message NETSDK1062")) return; _logger?.WriteLine("... store: " + e.Data); }); int exitCode; using (var proc = new Process()) { proc.StartInfo = psi; proc.Start(); proc.ErrorDataReceived += handler; proc.OutputDataReceived += handler; proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); proc.EnableRaisingEvents = true; proc.WaitForExit(); exitCode = proc.ExitCode; } return exitCode; } /// /// Executes the dotnet publish command for the provided project /// /// /// /// /// /// /// /// public int Publish(LambdaToolsDefaults defaults, string projectLocation, string outputLocation, string targetFramework, string configuration, string msbuildParameters, string architecture, IList publishManifests) { if (outputLocation == null) throw new ArgumentNullException(nameof(outputLocation)); if (Directory.Exists(outputLocation)) { try { Directory.Delete(outputLocation, true); _logger?.WriteLine("Deleted previous publish folder"); } catch (Exception e) { _logger?.WriteLine($"Warning unable to delete previous publish folder: {e.Message}"); } } _logger?.WriteLine($"... invoking 'dotnet publish', working folder '{outputLocation}'"); var dotnetCLI = FindExecutableInPath("dotnet.exe"); if (dotnetCLI == null) dotnetCLI = FindExecutableInPath("dotnet"); if (string.IsNullOrEmpty(dotnetCLI)) throw new Exception("Failed to locate dotnet CLI executable. Make sure the dotnet CLI is installed in the environment PATH."); var fullProjectLocation = this._workingDirectory; if (!string.IsNullOrEmpty(projectLocation)) { fullProjectLocation = Utilities.DetermineProjectLocation(this._workingDirectory, projectLocation); } var arguments = GetPublishArguments(fullProjectLocation, outputLocation, targetFramework, configuration, msbuildParameters, architecture, publishManifests); // echo the full dotnet command for debug _logger?.WriteLine($"... dotnet {arguments}"); var psi = new ProcessStartInfo { FileName = dotnetCLI, Arguments = arguments, WorkingDirectory = this._workingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; var handler = (DataReceivedEventHandler)((o, e) => { if (string.IsNullOrEmpty(e.Data)) return; _logger?.WriteLine("... publish: " + e.Data); }); int exitCode; using (var proc = new Process()) { proc.StartInfo = psi; proc.Start(); proc.ErrorDataReceived += handler; proc.OutputDataReceived += handler; proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); proc.EnableRaisingEvents = true; proc.WaitForExit(); exitCode = proc.ExitCode; } if (exitCode == 0) { ProcessAdditionalFiles(defaults, outputLocation); var chmodPath = FindExecutableInPath("chmod"); if (!string.IsNullOrEmpty(chmodPath) && File.Exists(chmodPath)) { // as we are not invoking through a shell, which would handle // wildcard expansion for us, we need to invoke per-file var files = Directory.GetFiles(outputLocation, "*", SearchOption.TopDirectoryOnly); foreach (var file in files) { var filename = Path.GetFileName(file); var psiChmod = new ProcessStartInfo { FileName = chmodPath, Arguments = "+rx \"" + filename + "\"", WorkingDirectory = outputLocation, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using (var proc = new Process()) { proc.StartInfo = psiChmod; proc.Start(); proc.ErrorDataReceived += handler; proc.OutputDataReceived += handler; proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); proc.EnableRaisingEvents = true; proc.WaitForExit(); if (proc.ExitCode == 0) { this._logger?.WriteLine($"Changed permissions on published file (chmod +rx {filename})."); } } } } } return exitCode; } public string GetPublishArguments(string projectLocation, string outputLocation, string targetFramework, string configuration, string msbuildParameters, string architecture, IList publishManifests, bool isNativeAot = false, string projectLocationInsideContainer = null) { StringBuilder arguments = new StringBuilder("publish"); if (!string.IsNullOrEmpty(projectLocationInsideContainer)) { arguments.Append($" \"{projectLocationInsideContainer}\""); } else if (!string.IsNullOrEmpty(projectLocation)) { arguments.Append($" \"{projectLocation}\""); } if (!string.IsNullOrEmpty(outputLocation)) { arguments.Append($" --output \"{outputLocation}\""); } if (!string.IsNullOrEmpty(configuration)) { arguments.Append($" --configuration \"{configuration}\""); } if (!string.IsNullOrEmpty(targetFramework)) { arguments.Append($" --framework \"{targetFramework}\""); } if (!string.IsNullOrEmpty(msbuildParameters)) { arguments.Append($" {msbuildParameters}"); } if (!string.Equals("netcoreapp1.0", targetFramework, StringComparison.OrdinalIgnoreCase)) { arguments.Append(" /p:GenerateRuntimeConfigurationFiles=true"); // Define an action to set the runtime and self-contained switches. var applyRuntimeSwitchAction = (Action)(() => { if (msbuildParameters == null || msbuildParameters.IndexOf("--runtime", StringComparison.InvariantCultureIgnoreCase) == -1) { arguments.Append($" --runtime {LambdaUtilities.DetermineRuntimeParameter(targetFramework, architecture)}"); } if (!Utilities.HasExplicitSelfContainedFlag(projectLocation, msbuildParameters)) { arguments.Append($" --self-contained {isNativeAot} "); } }); // This is here to not change existing behavior for the 2.0 and 2.1 runtimes. For those runtimes if // cshtml files are being used we need to support that cshtml being compiled at runtime. In order to do that we // need to not turn PreserveCompilationContext which provides reference assemblies to the runtime // compilation and not set a runtime. // // If there are no cshtml then disable PreserveCompilationContext to reduce package size and continue // to use the same runtime identifier that we used when those runtimes were launched. if (new string[] { "netcoreapp2.0", "netcoreapp2.1" }.Contains(targetFramework)) { if (Directory.GetFiles(projectLocation, "*.cshtml", SearchOption.AllDirectories).Length == 0) { applyRuntimeSwitchAction(); if (string.IsNullOrEmpty(msbuildParameters) || !msbuildParameters.Contains("PreserveCompilationContext")) { _logger?.WriteLine("... Disabling compilation context to reduce package size. If compilation context is needed pass in the \"/p:PreserveCompilationContext=false\" switch."); arguments.Append(" /p:PreserveCompilationContext=false"); } } } else { applyRuntimeSwitchAction(); } // If we have a manifest of packages already deploy in target deployment environment then write it to disk and add the // command line switch if (publishManifests != null && publishManifests.Count > 0) { foreach (var manifest in publishManifests) { arguments.Append($" --manifest \"{manifest}\""); } } if (isNativeAot) { // StripSymbols will greatly reduce output binary size with Native AOT arguments.Append(" /p:StripSymbols=true"); } } return arguments.ToString(); } private void ProcessAdditionalFiles(LambdaToolsDefaults defaults, string publishLocation) { var listOfDependencies = new List(); var extraDependences = defaults["additional-files"] as string[]; if (extraDependences != null) { foreach (var item in extraDependences) listOfDependencies.Add(item); } foreach (var relativePath in listOfDependencies) { var fileName = Path.GetFileName(relativePath); string source; if (Path.IsPathRooted(relativePath)) source = relativePath; else source = Path.Combine(publishLocation, relativePath); var target = Path.Combine(publishLocation, fileName); if (File.Exists(source) && !File.Exists(target)) { File.Copy(source, target); _logger?.WriteLine($"... publish: Adding additional file {relativePath}"); } } } /// /// A collection of known paths for common utilities that are usually not found in the path /// static readonly IDictionary KNOWN_LOCATIONS = new Dictionary(StringComparer.OrdinalIgnoreCase) { {"dotnet.exe", @"C:\Program Files\dotnet\dotnet.exe" }, {"chmod", @"/bin/chmod" }, {"zip", @"/usr/bin/zip" } }; /// /// Search the path environment variable for the command given. /// /// The command to search for in the path /// The full path to the command if found otherwise it will return null public static string FindExecutableInPath(string command) { if (File.Exists(command)) return Path.GetFullPath(command); #if NETCOREAPP3_1_OR_GREATER if (string.Equals(command, "dotnet.exe")) { if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { command = "dotnet"; } var mainModule = Process.GetCurrentProcess().MainModule; if (!string.IsNullOrEmpty(mainModule?.FileName) && Path.GetFileName(mainModule.FileName).Equals(command, StringComparison.OrdinalIgnoreCase)) { return mainModule.FileName; } } #endif Func quoteRemover = x => { if (x.StartsWith("\"")) x = x.Substring(1); if (x.EndsWith("\"")) x = x.Substring(0, x.Length - 1); return x; }; var envPath = Environment.GetEnvironmentVariable("PATH"); if (envPath != null) { foreach (var path in envPath.Split(Path.PathSeparator)) { try { var fullPath = Path.Combine(quoteRemover(path), command); if (File.Exists(fullPath)) return fullPath; } catch (Exception) { // Catch exceptions and continue if there are invalid characters in the user's path. } } } if (KNOWN_LOCATIONS.ContainsKey(command) && File.Exists(KNOWN_LOCATIONS[command])) return KNOWN_LOCATIONS[command]; return null; } } }