From 686434d35261d35e4edc95161ed86d667e7ef906 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 4 Dec 2025 15:19:17 -0800 Subject: [PATCH 01/11] Add support for using file-based C# Lambda functions --- .gitignore | 1 + aws-extensions-for-dotnet-cli.sln | 12 +- .../Commands/BaseCommand.cs | 18 +- .../Utilities.cs | 132 +++++--- .../Commands/DeployFunctionCommand.cs | 77 +++-- .../Commands/PackageCommand.cs | 41 ++- .../LambdaDefinedCommandOptions.cs | 2 +- src/Amazon.Lambda.Tools/LambdaPackager.cs | 50 +-- src/Amazon.Lambda.Tools/LambdaUtilities.cs | 20 +- src/Amazon.Lambda.Tools/Program.cs | 2 +- .../TemplateProcessorManager.cs | 14 +- .../DeployProjectTests.cs | 55 +++- .../DeployServerlessTests.cs | 63 ++++ .../LambdaSingleFilePackageTests.cs | 302 ++++++++++++++++++ .../UtilitiesTests.cs | 2 + .../ToUpperFunctionImplicitAOT.cs | 27 ++ .../ToUpperFunctionNoAOT.cs | 28 ++ .../serverless.template | 23 ++ 18 files changed, 767 insertions(+), 102 deletions(-) create mode 100644 test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs create mode 100644 testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs create mode 100644 testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs create mode 100644 testapps/SingeFileLambdaFunctions/serverless.template diff --git a/.gitignore b/.gitignore index 47e5a1fe..b35298af 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ **/node_modules/ **/TestGenerations/ +**/artifacts/ **/.vscode **/.idea diff --git a/aws-extensions-for-dotnet-cli.sln b/aws-extensions-for-dotnet-cli.sln index 28335904..d787390d 100644 --- a/aws-extensions-for-dotnet-cli.sln +++ b/aws-extensions-for-dotnet-cli.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32228.430 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11222.16 d18.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A5EA2C7D-846F-4266-8423-EFC4BA0FE4B6}" EndProject @@ -69,6 +69,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestIntegerFunction", "test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFunctionBuildProps", "testapps\TestFunctionBuildProps\TestFunctionBuildProps\TestFunctionBuildProps.csproj", "{AFA71B8E-F0AA-4704-8C4E-C11130F82B13}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SingeFileLambdaFunctions", "SingeFileLambdaFunctions", "{5711F48F-5491-4C8A-92B2-4D6D849483B5}" + ProjectSection(SolutionItems) = preProject + testapps\SingeFileLambdaFunctions\serverless.template = testapps\SingeFileLambdaFunctions\serverless.template + testapps\SingeFileLambdaFunctions\ToUpperFunctionImplicitAOT.cs = testapps\SingeFileLambdaFunctions\ToUpperFunctionImplicitAOT.cs + testapps\SingeFileLambdaFunctions\ToUpperFunctionNoAOT.cs = testapps\SingeFileLambdaFunctions\ToUpperFunctionNoAOT.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -207,6 +214,7 @@ Global {69FFA03C-D29F-40E0-9E7F-572D5E10AF77} = {BB3CF729-8213-4DDD-85AE-A5E7754F3944} {D7F1DFA4-066B-469C-B04C-DF032CF152C1} = {BB3CF729-8213-4DDD-85AE-A5E7754F3944} {AFA71B8E-F0AA-4704-8C4E-C11130F82B13} = {BB3CF729-8213-4DDD-85AE-A5E7754F3944} + {5711F48F-5491-4C8A-92B2-4D6D849483B5} = {BB3CF729-8213-4DDD-85AE-A5E7754F3944} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DBFC70D6-49A2-40A1-AB08-5D9504AB7112} diff --git a/src/Amazon.Common.DotNetCli.Tools/Commands/BaseCommand.cs b/src/Amazon.Common.DotNetCli.Tools/Commands/BaseCommand.cs index 0312a34f..1696d808 100644 --- a/src/Amazon.Common.DotNetCli.Tools/Commands/BaseCommand.cs +++ b/src/Amazon.Common.DotNetCli.Tools/Commands/BaseCommand.cs @@ -30,7 +30,17 @@ public abstract class BaseCommand : ICommand public BaseCommand(IToolLogger logger, string workingDirectory) { this.Logger = logger; - this.WorkingDirectory = workingDirectory; + + // In case working directory was set to a file then use the parent directory as the working directory. + // This can happen in the C# single file scenario. + if (File.Exists(workingDirectory)) + { + this.WorkingDirectory = Directory.GetParent(workingDirectory).FullName; + } + else + { + this.WorkingDirectory = workingDirectory; + } } public BaseCommand(IToolLogger logger, string workingDirectory, IList possibleOptions, string[] args) @@ -753,7 +763,11 @@ private int WaitForIndexResponse(int min, int max) public IToolLogger Logger { get; protected set; } - public string WorkingDirectory { get; set; } + public string WorkingDirectory + { + get; + set; + } protected string GetUserAgentString() { diff --git a/src/Amazon.Common.DotNetCli.Tools/Utilities.cs b/src/Amazon.Common.DotNetCli.Tools/Utilities.cs index 69b6685b..cfd3843e 100644 --- a/src/Amazon.Common.DotNetCli.Tools/Utilities.cs +++ b/src/Amazon.Common.DotNetCli.Tools/Utilities.cs @@ -2,7 +2,9 @@ using Amazon.S3; using Amazon.S3.Model; using Amazon.S3.Transfer; +using Amazon.Util; using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -11,14 +13,11 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Linq; using System.Xml.XPath; -using Amazon.Util; -using System.Text.RegularExpressions; -using System.Collections; -using System.Xml; -using System.Text.Json; namespace Amazon.Common.DotNetCli.Tools { @@ -143,7 +142,7 @@ public static string RelativePathTo(string start, string relativeTo) for (index = lastCommonRoot + 1; index < relativeToDirs.Length; index++) { relativePath.Append(relativeToDirs[index]); - if(index + 1 < relativeToDirs.Length) + if (index + 1 < relativeToDirs.Length) { relativePath.Append("/"); } @@ -161,10 +160,16 @@ public static string GetSolutionDirectoryFullPath(string workingDirectory, strin { return Path.Combine(workingDirectory, givenSolutionDirectory).TrimEnd('\\', '/'); } - + return givenSolutionDirectory.TrimEnd('\\', '/'); } + // If we are using .NET 10 or later file based C# files then treat the parent directory as the solution folder. + if (IsSingleFileCSharpFile(projectLocation)) + { + return Directory.GetParent(projectLocation).FullName.TrimEnd('\\', '/'); + } + // If we weren't given a solution path, try to find one looking up from the project file. var currentDirectory = projectLocation; @@ -179,9 +184,9 @@ public static string GetSolutionDirectoryFullPath(string workingDirectory, strin { return currentDirectory.TrimEnd('\\', '/'); } - + DirectoryInfo dirInfo = Directory.GetParent(currentDirectory); - if ((dirInfo == null) || !dirInfo.Exists) + if ((dirInfo == null) || !dirInfo.Exists) { break; } @@ -203,11 +208,26 @@ public static string GetSolutionDirectoryFullPath(string workingDirectory, strin /// public static string DeterminePublishLocation(string workingDirectory, string projectLocation, string configuration, string targetFramework) { - var path = Path.Combine(DetermineProjectLocation(workingDirectory, projectLocation), - "bin", - configuration, - targetFramework, - "publish"); + string path; + if (IsSingleFileCSharpFile(projectLocation)) + { + // Do not rely on Directory.GetParent() because if this value will eventually run inside a container but + // the host is Windows the slashes and root will get messed up. + int forwardSlashPosition = projectLocation.LastIndexOf('\\'); + int backSlashPosition = projectLocation.LastIndexOf('/'); + int position = Math.Max(forwardSlashPosition, backSlashPosition); + var parentFolder = projectLocation.Substring(0, position); + + path = Path.Combine(parentFolder, "artifacts", Path.GetFileNameWithoutExtension(projectLocation)); + } + else + { + path = Path.Combine(DetermineProjectLocation(workingDirectory, projectLocation), + "bin", + configuration, + targetFramework, + "publish"); + } return path; } @@ -331,7 +351,8 @@ public static string LookupTargetFrameworkFromProjectFile(string projectLocation if (properties.TryGetValue("TargetFrameworks", out var targetFrameworks) && !string.IsNullOrEmpty(targetFrameworks)) { var frameworks = targetFrameworks.Split(';'); - if (frameworks.Length > 1 ){ + if (frameworks.Length > 1) + { return null; } return frameworks[0]; @@ -378,6 +399,25 @@ public static bool LookPublishAotFlag(string projectLocation, string msBuildPara } } + // By default file base C# files are published with Native AOT. Users can override this by adding a line that says "#:property PublishAot=false". + if (Utilities.IsSingleFileCSharpFile(projectLocation)) + { + var directiveRegex = new Regex(@"^\s*#\s*:\s*property\s+PublishAot\s*=\s*false\s*$", RegexOptions.IgnoreCase); + foreach (var rawLine in File.ReadLines(projectLocation)) + { + if (string.IsNullOrWhiteSpace(rawLine)) + continue; + + var m = directiveRegex.Match(rawLine); + if (m.Success) + { + return false; + } + } + + return true; + } + var properties = LookupProjectProperties(projectLocation, msBuildParameters, "PublishAot"); if (properties.TryGetValue("PublishAot", out var publishAot)) { @@ -407,8 +447,8 @@ private static string FindProjectFileInDirectory(string directory) { if (File.Exists(directory)) return directory; - - foreach (var ext in new [] { "*.csproj", "*.fsproj", "*.vbproj" }) + + foreach (var ext in new[] { "*.csproj", "*.fsproj", "*.vbproj" }) { var files = Directory.GetFiles(directory, ext, SearchOption.TopDirectoryOnly); if (files.Length == 1) @@ -447,7 +487,7 @@ public static string DetermineProjectLocation(string workingDirectory, string pr return location.TrimEnd('\\', '/'); } - + /// /// Determine where the dotnet build directory is. /// @@ -622,7 +662,7 @@ private static IDictionary GetFilesToIncludeInArchive(string pub { var relativePath = file.Substring(publishLocation.Length) .Replace(Path.DirectorySeparatorChar.ToString(), uniformDirectorySeparator.ToString()); - + if (relativePath[0] == uniformDirectorySeparator) relativePath = relativePath.Substring(1); @@ -707,7 +747,7 @@ private static void BundleWithZipCLI(string zipCLI, string zipArchivePath, strin } } } - + public static async Task ValidateBucketRegionAsync(IAmazonS3 s3Client, string s3Bucket) { string bucketRegion; @@ -715,14 +755,14 @@ public static async Task ValidateBucketRegionAsync(IAmazonS3 s3Client, string s3 { bucketRegion = await Utilities.GetBucketRegionAsync(s3Client, s3Bucket); } - catch(Exception e) + catch (Exception e) { Console.Error.WriteLine($"Warning: Unable to determine region for bucket {s3Bucket}, assuming bucket is in correct region: {e.Message}", ToolsException.CommonErrorCode.S3GetBucketLocation, e); return; } var configuredRegion = s3Client.Config.RegionEndpoint?.SystemName; - if(configuredRegion == null && !string.IsNullOrEmpty(s3Client.Config.ServiceURL)) + if (configuredRegion == null && !string.IsNullOrEmpty(s3Client.Config.ServiceURL)) { configuredRegion = AWSSDKUtils.DetermineRegion(s3Client.Config.ServiceURL); } @@ -731,7 +771,7 @@ public static async Task ValidateBucketRegionAsync(IAmazonS3 s3Client, string s3 // knows what they are doing. if (configuredRegion == null) return; - + if (!string.Equals(bucketRegion, configuredRegion)) { throw new ToolsException($"Error: S3 bucket must be in the same region as the configured region {configuredRegion}. {s3Bucket} is in the region {bucketRegion}.", ToolsException.CommonErrorCode.BucketInDifferentRegionThenClient); @@ -834,7 +874,7 @@ private static EventHandler CreateProgressHandler(IT EventHandler handler = ((s, e) => { if (e.PercentDone != percentToUpdateOn && e.PercentDone <= percentToUpdateOn) return; - + var increment = e.PercentDone % UPLOAD_PROGRESS_INCREMENT; if (increment == 0) increment = UPLOAD_PROGRESS_INCREMENT; @@ -844,14 +884,14 @@ private static EventHandler CreateProgressHandler(IT return handler; } - + private static EventHandler CreateTransferUtilityProgressHandler(IToolLogger logger) { var percentToUpdateOn = UPLOAD_PROGRESS_INCREMENT; EventHandler handler = ((s, e) => { if (e.PercentDone != percentToUpdateOn && e.PercentDone <= percentToUpdateOn) return; - + var increment = e.PercentDone % UPLOAD_PROGRESS_INCREMENT; if (increment == 0) increment = UPLOAD_PROGRESS_INCREMENT; @@ -861,7 +901,7 @@ private static EventHandler CreateTransferUtilityProgressHan return handler; } - + internal static int WaitForPromptResponseByIndex(int min, int max) { int chosenIndex = -1; @@ -881,8 +921,8 @@ internal static int WaitForPromptResponseByIndex(int min, int max) return chosenIndex; } - - + + static readonly string GENERIC_ASSUME_ROLE_POLICY = @" { @@ -936,8 +976,8 @@ public static ExecuteShellCommandResult ExecuteShellCommand(string workingDirect if (string.IsNullOrEmpty(e.Data)) return; capturedOutput.AppendLine(e.Data); - }); - + }); + using (var proc = new Process()) { proc.StartInfo = startInfo; @@ -955,7 +995,7 @@ public static ExecuteShellCommandResult ExecuteShellCommand(string workingDirect proc.WaitForExit(); return new ExecuteShellCommandResult(proc.ExitCode, capturedOutput.ToString()); - } + } } public static string ReadSecretFromConsole() @@ -978,7 +1018,7 @@ public static string ReadSecretFromConsole() } // i.Key > 31: Skip the initial ascii control characters like ESC and tab. The space character is 32. // KeyChar == '\u0000' if the key pressed does not correspond to a printable character, e.g. F1, Pause-Break, etc - else if ((int)i.Key > 31 && i.KeyChar != '\u0000') + else if ((int)i.Key > 31 && i.KeyChar != '\u0000') { code.Append(i.KeyChar); Console.Write("*"); @@ -986,8 +1026,8 @@ public static string ReadSecretFromConsole() } return code.ToString().Trim(); } - - + + public static void CopyDirectory(string sourceDirectory, string destinationDirectory, bool copySubDirectories) { // Get the subdirectories for the specified directory. @@ -999,7 +1039,7 @@ public static void CopyDirectory(string sourceDirectory, string destinationDirec "Source directory does not exist or could not be found: " + sourceDirectory); } - + // If the destination directory doesn't exist, create it. if (!Directory.Exists(destinationDirectory)) { @@ -1033,7 +1073,7 @@ public static bool TryGenerateECRRepositoryName(string projectName, out string r { projectName = new DirectoryInfo(projectName).Name; } - else if(File.Exists(projectName)) + else if (File.Exists(projectName)) { projectName = Path.GetFileNameWithoutExtension(projectName); } @@ -1041,20 +1081,20 @@ public static bool TryGenerateECRRepositoryName(string projectName, out string r projectName = projectName.ToLower(); var sb = new StringBuilder(); - foreach(var c in projectName) + foreach (var c in projectName) { - if(char.IsLetterOrDigit(c)) + if (char.IsLetterOrDigit(c)) { sb.Append(c); } - else if(sb.Length > 0 && (c == '.' || c == '_' || c == '-')) + else if (sb.Length > 0 && (c == '.' || c == '_' || c == '-')) { sb.Append(c); } } // Repository name must be at least 2 characters - if(sb.Length > 1) + if (sb.Length > 1) { repositoryName = sb.ToString(); @@ -1067,5 +1107,15 @@ public static bool TryGenerateECRRepositoryName(string projectName, out string r return !string.IsNullOrEmpty(repositoryName); } + + /// + /// Returns true if the project location is using the .NET feature of being a single file based C# file. + /// + /// + /// + public static bool IsSingleFileCSharpFile(string projectLocation) + { + return projectLocation.EndsWith(".cs", StringComparison.Ordinal); + } } } diff --git a/src/Amazon.Lambda.Tools/Commands/DeployFunctionCommand.cs b/src/Amazon.Lambda.Tools/Commands/DeployFunctionCommand.cs index 34e4805d..bd819858 100644 --- a/src/Amazon.Lambda.Tools/Commands/DeployFunctionCommand.cs +++ b/src/Amazon.Lambda.Tools/Commands/DeployFunctionCommand.cs @@ -12,7 +12,6 @@ using Amazon.Common.DotNetCli.Tools; using Amazon.Common.DotNetCli.Tools.Commands; using Amazon.Common.DotNetCli.Tools.Options; -using Amazon.Runtime.Internal; namespace Amazon.Lambda.Tools.Commands { @@ -25,7 +24,7 @@ public class DeployFunctionCommand : UpdateFunctionConfigCommand { public const string COMMAND_DEPLOY_NAME = "deploy-function"; public const string COMMAND_DEPLOY_DESCRIPTION = "Command to deploy the project to AWS Lambda"; - public const string COMMAND_DEPLOY_ARGUMENTS = " The name of the function to deploy"; + public const string COMMAND_DEPLOY_ARGUMENTS = " "; public static readonly IList DeployCommandOptions = BuildLineOptions(new List { @@ -114,6 +113,8 @@ public class DeployFunctionCommand : UpdateFunctionConfigCommand public string ContainerImageForBuild { get; set; } public string CodeMountDirectory { get; set; } + public string InputSingleCSharpFile { get; set; } + public DeployFunctionCommand(IToolLogger logger, string workingDirectory, string[] args) : base(logger, workingDirectory, DeployCommandOptions, args) { @@ -127,9 +128,20 @@ public DeployFunctionCommand(IToolLogger logger, string workingDirectory, string protected override void ParseCommandArguments(CommandOptions values) { base.ParseCommandArguments(values); + if (values.Arguments.Count > 0) { - this.FunctionName = values.Arguments[0]; + foreach (var arg in values.Arguments) + { + if (Utilities.IsSingleFileCSharpFile(arg)) + { + this.InputSingleCSharpFile = arg; + } + else + { + this.FunctionName = arg; + } + } } Tuple tuple; @@ -201,6 +213,9 @@ protected override async Task PerformActionAsync() var architecture = this.GetStringValueOrDefault(this.Architecture, LambdaDefinedCommandOptions.ARGUMENT_FUNCTION_ARCHITECTURE, false); + // Get the current Lambda configuration. If this returns null then the function does not exist and we will create it. + var currentConfiguration = await GetFunctionConfigurationAsync(); + Lambda.PackageType packageType = DeterminePackageType(); string ecrImageUri = null; @@ -222,7 +237,19 @@ protected override async Task PerformActionAsync() { if (string.IsNullOrEmpty(package)) { - EnsureInProjectDirectory(); + var lambdaRuntime = currentConfiguration?.Runtime ?? this.GetStringValueOrDefault(this.Runtime, LambdaDefinedCommandOptions.ARGUMENT_FUNCTION_RUNTIME, true); + + if (!string.IsNullOrEmpty(this.InputSingleCSharpFile)) + { + if (Path.IsPathFullyQualified(this.InputSingleCSharpFile)) + projectLocation = this.InputSingleCSharpFile; + else + projectLocation = Path.Combine(projectLocation, this.InputSingleCSharpFile); + } + else if (!Utilities.IsSingleFileCSharpFile(projectLocation)) + { + EnsureInProjectDirectory(); + } // Release will be the default configuration if nothing set. string configuration = this.GetStringValueOrDefault(this.Configuration, CommonDefinedCommandOptions.ARGUMENT_CONFIGURATION, false); @@ -231,11 +258,18 @@ protected override async Task PerformActionAsync() var targetFramework = this.GetStringValueOrDefault(this.TargetFramework, CommonDefinedCommandOptions.ARGUMENT_FRAMEWORK, false); if (string.IsNullOrEmpty(targetFramework)) { - targetFramework = Utilities.LookupTargetFrameworkFromProjectFile(projectLocation, msbuildParameters); + if (Utilities.IsSingleFileCSharpFile(projectLocation)) + { + targetFramework = LambdaUtilities.DetermineTargetFrameworkForSingleFile(lambdaRuntime); + } + else + { + targetFramework = Utilities.LookupTargetFrameworkFromProjectFile(projectLocation, msbuildParameters); + } // If we still don't know what the target framework is ask the user what targetframework to use. // This is common when a project is using multi targeting. - if(string.IsNullOrEmpty(targetFramework)) + if (string.IsNullOrEmpty(targetFramework)) { targetFramework = this.GetStringValueOrDefault(this.TargetFramework, CommonDefinedCommandOptions.ARGUMENT_FRAMEWORK, true); } @@ -243,7 +277,7 @@ protected override async Task PerformActionAsync() bool isNativeAot = Utilities.LookPublishAotFlag(projectLocation, this.MSBuildParameters); - ValidateTargetFrameworkAndLambdaRuntime(targetFramework); + LambdaUtilities.ValidateTargetFrameworkAndLambdaRuntime(lambdaRuntime, targetFramework); bool disableVersionCheck = this.GetBoolValueOrDefault(this.DisableVersionCheck, LambdaDefinedCommandOptions.ARGUMENT_DISABLE_VERSION_CHECK, false).GetValueOrDefault(); string publishLocation; @@ -306,9 +340,7 @@ protected override async Task PerformActionAsync() var s3Prefix = this.GetStringValueOrDefault(this.S3Prefix, LambdaDefinedCommandOptions.ARGUMENT_S3_PREFIX, false); s3Key = await Utilities.UploadToS3Async(this.Logger, this.S3Client, s3Bucket, s3Prefix, functionName, lambdaZipArchiveStream); } - - - var currentConfiguration = await GetFunctionConfigurationAsync(); + if (currentConfiguration == null) { this.Logger.WriteLine($"Creating new Lambda function {this.FunctionName}"); @@ -355,7 +387,22 @@ protected override async Task PerformActionAsync() if(packageType == Lambda.PackageType.Zip) { - createRequest.Handler = this.GetStringValueOrDefault(this.Handler, LambdaDefinedCommandOptions.ARGUMENT_FUNCTION_HANDLER, true); + string handler; + if (Utilities.IsSingleFileCSharpFile(projectLocation)) + { + handler = this.GetStringValueOrDefault(this.Handler, LambdaDefinedCommandOptions.ARGUMENT_FUNCTION_HANDLER, false); + if (string.IsNullOrEmpty(handler)) + { + // In a file based Lambda function it is always an executable and the assembly is the file name. + handler = Path.GetFileNameWithoutExtension(projectLocation); + } + } + else + { + handler = this.GetStringValueOrDefault(this.Handler, LambdaDefinedCommandOptions.ARGUMENT_FUNCTION_HANDLER, true); + } + + createRequest.Handler = handler; createRequest.Runtime = this.GetStringValueOrDefault(this.Runtime, LambdaDefinedCommandOptions.ARGUMENT_FUNCTION_RUNTIME, true); createRequest.Layers = layerVersionArns?.ToList(); @@ -578,14 +625,6 @@ private async Task PushLambdaImageAsync() return result; } - - - private void ValidateTargetFrameworkAndLambdaRuntime(string targetFramework) - { - string runtimeName = this.GetStringValueOrDefault(this.Runtime, LambdaDefinedCommandOptions.ARGUMENT_FUNCTION_RUNTIME, true); - LambdaUtilities.ValidateTargetFrameworkAndLambdaRuntime(runtimeName, targetFramework); - } - protected override void SaveConfigFile(JsonData data) { diff --git a/src/Amazon.Lambda.Tools/Commands/PackageCommand.cs b/src/Amazon.Lambda.Tools/Commands/PackageCommand.cs index 3999e699..2a85c34c 100644 --- a/src/Amazon.Lambda.Tools/Commands/PackageCommand.cs +++ b/src/Amazon.Lambda.Tools/Commands/PackageCommand.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Amazon.Common.DotNetCli.Tools; using Amazon.Common.DotNetCli.Tools.Commands; @@ -13,7 +14,7 @@ public class PackageCommand : LambdaBaseCommand public const string COMMAND_NAME = "package"; public const string COMMAND_DESCRIPTION = "Command to package a Lambda project either into a zip file or docker image if --package-type is set to \"image\". The output can later be deployed to Lambda " + "with either deploy-function command or with another tool."; - public const string COMMAND_ARGUMENTS = " The name of the zip file to package the project into"; + public const string COMMAND_ARGUMENTS = " "; public static readonly IList PackageCommandOptions = BuildLineOptions(new List { @@ -44,7 +45,8 @@ public class PackageCommand : LambdaBaseCommand public string Configuration { get; set; } public string TargetFramework { get; set; } public string OutputPackageFileName { get; set; } - + public string InputSingleCSharpFile { get; set; } + public string MSBuildParameters { get; set; } public string[] LayerVersionArns { get; set; } @@ -97,7 +99,17 @@ protected override void ParseCommandArguments(CommandOptions values) if (values.Arguments.Count > 0) { - this.OutputPackageFileName = values.Arguments[0]; + foreach (var arg in values.Arguments) + { + if (Utilities.IsSingleFileCSharpFile(arg)) + { + this.InputSingleCSharpFile = arg; + } + else + { + this.OutputPackageFileName = arg; + } + } } Tuple tuple; @@ -147,13 +159,23 @@ protected override void ParseCommandArguments(CommandOptions values) protected override async Task PerformActionAsync() { - EnsureInProjectDirectory(); - // Disable interactive since this command is intended to be run as part of a pipeline. this.DisableInteractive = true; string projectLocation = Utilities.DetermineProjectLocation(this.WorkingDirectory, this.GetStringValueOrDefault(this.ProjectLocation, CommonDefinedCommandOptions.ARGUMENT_PROJECT_LOCATION, false)); + if (!string.IsNullOrEmpty(this.InputSingleCSharpFile)) + { + if (Path.IsPathFullyQualified(this.InputSingleCSharpFile)) + projectLocation = this.InputSingleCSharpFile; + else + projectLocation = Path.Combine(projectLocation, this.InputSingleCSharpFile); + } + else if (!Utilities.IsSingleFileCSharpFile(projectLocation)) + { + EnsureInProjectDirectory(); + } + Lambda.PackageType packageType = LambdaUtilities.DeterminePackageType(this.GetStringValueOrDefault(this.PackageType, LambdaDefinedCommandOptions.ARGUMENT_PACKAGE_TYPE, false)); if(packageType == Lambda.PackageType.Image) { @@ -210,7 +232,14 @@ protected override async Task PerformActionAsync() var targetFramework = this.GetStringValueOrDefault(this.TargetFramework, CommonDefinedCommandOptions.ARGUMENT_FRAMEWORK, false); if (string.IsNullOrEmpty(targetFramework)) { - targetFramework = Utilities.LookupTargetFrameworkFromProjectFile(projectLocation, msbuildParameters); + if (Utilities.IsSingleFileCSharpFile(projectLocation)) + { + targetFramework = LambdaUtilities.DetermineTargetFrameworkForSingleFile(null); + } + else + { + targetFramework = Utilities.LookupTargetFrameworkFromProjectFile(projectLocation, msbuildParameters); + } // If we still don't know what the target framework is ask the user what targetframework to use. // This is common when a project is using multi targeting. diff --git a/src/Amazon.Lambda.Tools/LambdaDefinedCommandOptions.cs b/src/Amazon.Lambda.Tools/LambdaDefinedCommandOptions.cs index 7df2b81b..1e8dfdc1 100644 --- a/src/Amazon.Lambda.Tools/LambdaDefinedCommandOptions.cs +++ b/src/Amazon.Lambda.Tools/LambdaDefinedCommandOptions.cs @@ -61,7 +61,7 @@ public static class LambdaDefinedCommandOptions ShortSwitch = "-fh", Switch = "--function-handler", ValueType = CommandOption.CommandOptionValueType.StringValue, - Description = "Handler for the function ::::" + Description = "Handler for the function. .NET Class libraries use the format :::: and executables use the format " }; public static readonly CommandOption ARGUMENT_PACKAGE_TYPE = new CommandOption diff --git a/src/Amazon.Lambda.Tools/LambdaPackager.cs b/src/Amazon.Lambda.Tools/LambdaPackager.cs index ae0382e4..9199f042 100644 --- a/src/Amazon.Lambda.Tools/LambdaPackager.cs +++ b/src/Amazon.Lambda.Tools/LambdaPackager.cs @@ -217,36 +217,40 @@ public static bool CreateApplicationBundle(LambdaToolsDefaults defaults, IToolLo } } - var buildLocation = Utilities.DetermineBuildLocation(workingDirectory, projectLocation, configuration, targetFramework); - - // This is here for legacy reasons. Some older versions of the dotnet CLI were not - // copying the deps.json file into the publish folder. - foreach (var file in Directory.GetFiles(buildLocation, "*.deps.json", SearchOption.TopDirectoryOnly)) - { - var destinationPath = Path.Combine(publishLocation, Path.GetFileName(file)); - if (!File.Exists(destinationPath)) - File.Copy(file, destinationPath); - } - bool flattenRuntime = false; - var depsJsonTargetNode = GetDepsJsonTargetNode(logger, publishLocation); - // If there is no target node then this means the tool is being used on a future version of .NET Core - // then was available when the this tool was written. Go ahead and continue the deployment with warnings so the - // user can see if the future version will work. - if (depsJsonTargetNode != null && string.Equals(targetFramework, "netcoreapp1.0", StringComparison.OrdinalIgnoreCase)) + // The code inside this block is for old dotnet CLI issues that would not be a factor + // in a dotnet CLI that supports file based csharp files. + if (!Utilities.IsSingleFileCSharpFile(projectLocation)) { - // Make sure the project is not pulling in dependencies requiring a later version of .NET Core then the declared target framework - if (!ValidateDependencies(logger, targetFramework, depsJsonTargetNode, disableVersionCheck)) - return false; + var buildLocation = Utilities.DetermineBuildLocation(workingDirectory, projectLocation, configuration, targetFramework); - // Flatten the runtime folder which reduces the package size by not including native dependencies - // for other platforms. - flattenRuntime = FlattenRuntimeFolder(logger, publishLocation, depsJsonTargetNode); + // This is here for legacy reasons. Some older versions of the dotnet CLI were not + // copying the deps.json file into the publish folder. + foreach (var file in Directory.GetFiles(buildLocation, "*.deps.json", SearchOption.TopDirectoryOnly)) + { + var destinationPath = Path.Combine(publishLocation, Path.GetFileName(file)); + if (!File.Exists(destinationPath)) + File.Copy(file, destinationPath); + } + + var depsJsonTargetNode = GetDepsJsonTargetNode(logger, publishLocation); + // If there is no target node then this means the tool is being used on a future version of .NET Core + // then was available when the this tool was written. Go ahead and continue the deployment with warnings so the + // user can see if the future version will work. + if (depsJsonTargetNode != null && string.Equals(targetFramework, "netcoreapp1.0", StringComparison.OrdinalIgnoreCase)) + { + // Make sure the project is not pulling in dependencies requiring a later version of .NET Core then the declared target framework + if (!ValidateDependencies(logger, targetFramework, depsJsonTargetNode, disableVersionCheck)) + return false; + + // Flatten the runtime folder which reduces the package size by not including native dependencies + // for other platforms. + flattenRuntime = FlattenRuntimeFolder(logger, publishLocation, depsJsonTargetNode); + } } FlattenPowerShellRuntimeModules(logger, publishLocation, targetFramework); - if (zipArchivePath == null) zipArchivePath = Path.Combine(Directory.GetParent(publishLocation).FullName, new DirectoryInfo(projectLocation).Name + ".zip"); diff --git a/src/Amazon.Lambda.Tools/LambdaUtilities.cs b/src/Amazon.Lambda.Tools/LambdaUtilities.cs index 65c7f82c..bcfec970 100644 --- a/src/Amazon.Lambda.Tools/LambdaUtilities.cs +++ b/src/Amazon.Lambda.Tools/LambdaUtilities.cs @@ -26,6 +26,7 @@ namespace Amazon.Lambda.Tools { public static class TargetFrameworkMonikers { + public const string net10_0 = "net10.0"; public const string net90 = "net9.0"; public const string net80 = "net8.0"; public const string net70 = "net7.0"; @@ -58,6 +59,7 @@ public static class LambdaUtilities public static readonly IReadOnlyDictionary _lambdaRuntimeToDotnetFramework = new Dictionary() { + {"dotnet10", TargetFrameworkMonikers.net10_0}, {Amazon.Lambda.Runtime.Dotnet8.Value, TargetFrameworkMonikers.net80}, {Amazon.Lambda.Runtime.Dotnet6.Value, TargetFrameworkMonikers.net60}, {Amazon.Lambda.Runtime.Dotnetcore31.Value, TargetFrameworkMonikers.netcoreapp31}, @@ -85,12 +87,26 @@ public static string DetermineLambdaRuntimeFromTargetFramework(string targetFram return kvp.Key; } + public static string DetermineTargetFrameworkForSingleFile(string lambdaRuntime) + { + string targetFramework; + if (lambdaRuntime != null && _lambdaRuntimeToDotnetFramework.TryGetValue(lambdaRuntime, out targetFramework)) + { + return targetFramework; + } + + // TODO: Figure out what to do when we don't have a lambdaRuntime to base it on. + targetFramework = "net10.0"; + + return targetFramework; + } + public static void ValidateTargetFramework(string projectLocation, string msbuildParameters, string targetFramework, bool isNativeAot) { var outputType = Utilities.LookupOutputTypeFromProjectFile(projectLocation, msbuildParameters); var ouputTypeIsExe = outputType != null && outputType.ToLower().Equals("exe"); - if (isNativeAot && !ouputTypeIsExe) + if (isNativeAot && !ouputTypeIsExe && !Utilities.IsSingleFileCSharpFile(projectLocation)) { throw new LambdaToolsException($"Native AOT applications must have output type 'exe'.", LambdaToolsException.LambdaErrorCode.NativeAotOutputTypeError); @@ -123,6 +139,8 @@ public static string GetDefaultBuildImage(string targetFramework, string archite switch (targetFramework?.ToLower()) { + case TargetFrameworkMonikers.net10_0: + return $"mcr.microsoft.com/dotnet/sdk:10.0-aot"; case TargetFrameworkMonikers.net90: return $"public.ecr.aws/sam/build-dotnet9:latest-{architecture}"; case TargetFrameworkMonikers.net80: diff --git a/src/Amazon.Lambda.Tools/Program.cs b/src/Amazon.Lambda.Tools/Program.cs index 841937b8..497ba16b 100644 --- a/src/Amazon.Lambda.Tools/Program.cs +++ b/src/Amazon.Lambda.Tools/Program.cs @@ -14,7 +14,7 @@ static void Main(string[] args) new List() { new GroupHeaderInfo("Commands to deploy and manage AWS Lambda functions:"), - new CommandInfo(DeployFunctionCommand.COMMAND_DEPLOY_NAME, DeployFunctionCommand.COMMAND_DEPLOY_DESCRIPTION, DeployFunctionCommand.DeployCommandOptions, DeployFunctionCommand.COMMAND_ARGUMENTS), + new CommandInfo(DeployFunctionCommand.COMMAND_DEPLOY_NAME, DeployFunctionCommand.COMMAND_DEPLOY_DESCRIPTION, DeployFunctionCommand.DeployCommandOptions, DeployFunctionCommand.COMMAND_DEPLOY_ARGUMENTS), new CommandInfo(InvokeFunctionCommand.COMMAND_NAME, InvokeFunctionCommand.COMMAND_DESCRIPTION, InvokeFunctionCommand.InvokeCommandOptions, InvokeFunctionCommand.COMMAND_ARGUMENTS), new CommandInfo(ListFunctionCommand.COMMAND_NAME, ListFunctionCommand.COMMAND_DESCRIPTION, ListFunctionCommand.ListCommandOptions), new CommandInfo(DeleteFunctionCommand.COMMAND_NAME, DeleteFunctionCommand.COMMAND_DESCRIPTION, DeleteFunctionCommand.DeleteCommandOptions, DeleteFunctionCommand.COMMAND_ARGUMENTS), diff --git a/src/Amazon.Lambda.Tools/TemplateProcessor/TemplateProcessorManager.cs b/src/Amazon.Lambda.Tools/TemplateProcessor/TemplateProcessorManager.cs index 03b53a7a..ba217812 100644 --- a/src/Amazon.Lambda.Tools/TemplateProcessor/TemplateProcessorManager.cs +++ b/src/Amazon.Lambda.Tools/TemplateProcessor/TemplateProcessorManager.cs @@ -153,7 +153,7 @@ private async Task ProcessUpdatableResourceAsync(string t results = new UpdateResourceResults { ImageUri = localPath }; } // Uploading a single file as the code for the resource. If the single file is not a zip file then zip the file first. - else if (File.Exists(localPath)) + else if (File.Exists(localPath) && !Utilities.IsSingleFileCSharpFile(localPath)) { if(field.IsCode && !string.Equals(Path.GetExtension(localPath), ".zip", StringComparison.OrdinalIgnoreCase)) { @@ -172,7 +172,7 @@ private async Task ProcessUpdatableResourceAsync(string t { throw new LambdaToolsException($"File that the field {field.Resource.Name}/{field.Name} is pointing to doesn't exist", LambdaToolsException.LambdaErrorCode.ServerlessTemplateMissingLocalPath); } - else if (!Directory.Exists(localPath)) + else if (!Directory.Exists(localPath) && !Utilities.IsSingleFileCSharpFile(localPath)) { throw new LambdaToolsException($"Directory that the field {field.Resource.Name}/{field.Name} is pointing doesn't exist", LambdaToolsException.LambdaErrorCode.ServerlessTemplateMissingLocalPath); } @@ -187,7 +187,7 @@ private async Task ProcessUpdatableResourceAsync(string t // If the function is image upload then run the .NET tools to handle running // docker build even if the current folder is not a .NET project. The .NET // could be in a sub folder or be a self contained Docker build. - if (IsDotnetProjectDirectory(localPath) || field.Resource.UploadType == CodeUploadType.Image) + if (RequiredDotnetPackaging(localPath) || field.Resource.UploadType == CodeUploadType.Image) { results = await PackageDotnetProjectAsync(field, localPath, args); } @@ -240,8 +240,9 @@ private async Task PackageDotnetProjectAsync(IUpdateResou { if (field.Resource.UploadType == CodeUploadType.Zip) { - var command = new Commands.PackageCommand(this.Logger, location, args); + var command = new Commands.PackageCommand(this.Logger, null, args); + command.ProjectLocation = location; command.LambdaClient = this.OriginatingCommand?.LambdaClient; command.S3Client = this.OriginatingCommand?.S3Client; command.IAMClient = this.OriginatingCommand?.IAMClient; @@ -365,8 +366,11 @@ private static string GenerateOutputZipFilename(IUpdateResourceField field) /// /// /// - private bool IsDotnetProjectDirectory(string localPath) + private bool RequiredDotnetPackaging(string localPath) { + if (Utilities.IsSingleFileCSharpFile(localPath)) + return true; + if (!Directory.Exists(localPath)) return false; diff --git a/test/Amazon.Lambda.Tools.Integ.Tests/DeployProjectTests.cs b/test/Amazon.Lambda.Tools.Integ.Tests/DeployProjectTests.cs index b4d43300..547c8842 100644 --- a/test/Amazon.Lambda.Tools.Integ.Tests/DeployProjectTests.cs +++ b/test/Amazon.Lambda.Tools.Integ.Tests/DeployProjectTests.cs @@ -1,3 +1,5 @@ +using Amazon.CloudFormation; +using Amazon.CloudFormation.Model; using Amazon.Lambda.Model; using Amazon.Lambda.Tools.Commands; using System; @@ -7,7 +9,6 @@ using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; - using static Amazon.Lambda.Tools.Integ.Tests.TestConstants; namespace Amazon.Lambda.Tools.Integ.Tests @@ -334,6 +335,58 @@ public async Task SetAndClearEnvironmentVariables() } } + [Fact] + public async Task TestDeploySingleCSharpFile() + { + var assembly = this.GetType().GetTypeInfo().Assembly; + var toolLogger = new TestToolLogger(_testOutputHelper); + var csharpFile = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + + var functionName = "TestDeploySingleCSharpFile-" + DateTime.Now.Ticks; + var deployFunctionCommand = new DeployFunctionCommand(toolLogger, System.Environment.CurrentDirectory, new string[] { csharpFile }); + deployFunctionCommand.Role = await TestHelper.GetTestRoleArnAsync(); + deployFunctionCommand.DisableInteractive = true; + deployFunctionCommand.Runtime = "dotnet10"; + deployFunctionCommand.Timeout = 30; + deployFunctionCommand.MemorySize = 512; + deployFunctionCommand.Region = TEST_REGION; + deployFunctionCommand.Configuration = "Release"; + deployFunctionCommand.FunctionName = functionName; + + var created = false; + try + { + created = await deployFunctionCommand.ExecuteAsync(); + Assert.True(created); + + toolLogger.ClearBuffer(); + var invokeCommand = new InvokeFunctionCommand(toolLogger, System.Environment.CurrentDirectory, new string[0]); + invokeCommand.FunctionName = functionName; + invokeCommand.Payload = "hello world"; + invokeCommand.Region = TEST_REGION; + + await invokeCommand.ExecuteAsync(); + Assert.Contains("HELLO WORLD", toolLogger.Buffer); + } + finally + { + if (created) + { + try + { + var deleteCommand = new DeleteFunctionCommand(toolLogger, System.Environment.CurrentDirectory, new string[0]); + deleteCommand.FunctionName = functionName; + deleteCommand.Region = TEST_REGION; + await deleteCommand.ExecuteAsync(); + } + catch + { + // Bury exception because we don't want to lose any exceptions during the deploy stage. + } + } + } + } + [Fact] public async Task DeployWithFunctionUrl() { diff --git a/test/Amazon.Lambda.Tools.Integ.Tests/DeployServerlessTests.cs b/test/Amazon.Lambda.Tools.Integ.Tests/DeployServerlessTests.cs index 883b01c5..242d545c 100644 --- a/test/Amazon.Lambda.Tools.Integ.Tests/DeployServerlessTests.cs +++ b/test/Amazon.Lambda.Tools.Integ.Tests/DeployServerlessTests.cs @@ -281,6 +281,69 @@ public async Task TestDeployServerlessECRImageUriNoMetadataYamlTemplate() } } + [Fact] + public async Task TestDeployServerlessReferencingSingleFile() + { + var assembly = this.GetType().GetTypeInfo().Assembly; + var toolLogger = new TestToolLogger(_testOutputHelper); + var templatePath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/serverless.template"); + + var stackName = "TestDeployServerlessReferencingSingleFile-" + DateTime.Now.Ticks; + var deployServerlessCommand = new DeployServerlessCommand(toolLogger, Environment.CurrentDirectory, new string[] { "--template", templatePath }); + deployServerlessCommand.DisableInteractive = true; + deployServerlessCommand.Region = TEST_REGION; + deployServerlessCommand.Configuration = "Release"; + deployServerlessCommand.StackName = stackName; + deployServerlessCommand.S3Bucket = this._testFixture.Bucket; + deployServerlessCommand.WaitForStackToComplete = true; + + var created = false; + try + { + string functionName = null; + created = await deployServerlessCommand.ExecuteAsync(); + Assert.True(created); + using (var cfClient = new AmazonCloudFormationClient(RegionEndpoint.GetBySystemName(TEST_REGION))) + { + var describeResponse = await cfClient.DescribeStacksAsync(new DescribeStacksRequest + { + StackName = deployServerlessCommand.StackName + }); + + Assert.Equal(StackStatus.CREATE_COMPLETE, describeResponse.Stacks[0].StackStatus); + + var describeResourceResponse = await cfClient.DescribeStackResourceAsync(new DescribeStackResourceRequest { StackName = stackName, LogicalResourceId = "ToUpperFunctionNoAOT" }); + functionName = describeResourceResponse.StackResourceDetail.PhysicalResourceId; + } + + toolLogger.ClearBuffer(); + var invokeCommand = new InvokeFunctionCommand(toolLogger, Environment.CurrentDirectory, new string[0]); + invokeCommand.FunctionName = functionName; + invokeCommand.Payload = "hello world"; + invokeCommand.Region = TEST_REGION; + + await invokeCommand.ExecuteAsync(); + Assert.Contains("HELLO WORLD", toolLogger.Buffer); + } + finally + { + if (created) + { + try + { + var deleteCommand = new DeleteServerlessCommand(toolLogger, Environment.CurrentDirectory, new string[0]); + deleteCommand.StackName = deployServerlessCommand.StackName; + deleteCommand.Region = TEST_REGION; + await deleteCommand.ExecuteAsync(); + } + catch + { + // Bury exception because we don't want to lose any exceptions during the deploy stage. + } + } + } + } + [Fact] public async Task TestDeployServerlessECRImageUriNoMetadataJsonTemplate() { diff --git a/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs b/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs new file mode 100644 index 00000000..22212054 --- /dev/null +++ b/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs @@ -0,0 +1,302 @@ +using Amazon.Common.DotNetCli.Tools; +using Amazon.Lambda.Tools.Commands; +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Amazon.Lambda.Tools.Test +{ + [Collection("SingleFileTests")] + public class LambdaSingleFilePackageTests + { + private readonly ITestOutputHelper _testOutputHelper; + + public LambdaSingleFilePackageTests(ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = testOutputHelper; + } + + [Fact] + public async Task PackageToUpperNoAOTSettingWithArgument() + { + var tempFile = Path.GetTempFileName() + ".zip"; + try + { + var assembly = this.GetType().GetTypeInfo().Assembly; + var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + var command = new PackageCommand(new TestToolLogger(_testOutputHelper), Environment.CurrentDirectory, new string[] {fullPath, tempFile }); + + var created = await command.ExecuteAsync(); + Assert.True(created); + + Assert.True(File.Exists(tempFile)); + Assert.True(new FileInfo(tempFile).Length > 0); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + [Fact] + public async Task PackageToUpperNoAOTSettingWithProjectLocation() + { + var tempFile = Path.GetTempFileName() + ".zip"; + try + { + var assembly = this.GetType().GetTypeInfo().Assembly; + var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + var command = new PackageCommand(new TestToolLogger(_testOutputHelper), Environment.CurrentDirectory, new string[] { tempFile, "--project-location", fullPath }); + + var created = await command.ExecuteAsync(); + Assert.True(created); + + Assert.True(File.Exists(tempFile)); + Assert.True(new FileInfo(tempFile).Length > 0); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + [Theory] + [InlineData("ToUpperFunctionNoAOT.cs", false, "")] + [InlineData("ToUpperFunctionImplicitAOT.cs", true, "")] + [InlineData("ToUpperFunctionNoAOT.cs", true, "/p:publishaot=true")] + [InlineData("ToUpperFunctionImplicitAOT.cs", false, "/p:publishaot=false")] + public void ConfirmUsingNativeAOT(string filename, bool isAot, string msBuildParameters) + { + var assembly = this.GetType().GetTypeInfo().Assembly; + var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + $"../../../../../../testapps/SingeFileLambdaFunctions/{filename}"); + var actualAot = Utilities.LookPublishAotFlag(fullPath, msBuildParameters); + + Assert.Equal(isAot, actualAot); + } + + [Fact] + public void DeterminePublishLocationForSingleFile() + { + string fullPath; + string expectedPublishLocation; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fullPath = @"C:\functions\helloworld.cs"; + expectedPublishLocation = @"C:/functions/artifacts/helloworld"; + } + else + { + fullPath = @"/functions/helloworld.cs"; + expectedPublishLocation = @"/functions/artifacts/helloworld"; + } + + var publishLocation = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, fullPath, "Release", "net10.0"); + Assert.Equal(expectedPublishLocation, publishLocation.Replace("\\", "/")); + } + + [Fact] + public void DeterminePublishLocationForSingleFileWithMixedSlashes() + { + string fullPath; + string expectedPublishLocation; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fullPath = @"C:\functions/subfolder\myfunction.cs"; + expectedPublishLocation = @"C:/functions/subfolder/artifacts/myfunction"; + } + else + { + fullPath = @"/functions/subfolder/myfunction.cs"; + expectedPublishLocation = @"/functions/subfolder/artifacts/myfunction"; + } + + var publishLocation = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, fullPath, "Release", "net10.0"); + Assert.Equal(expectedPublishLocation, publishLocation.Replace("\\", "/")); + } + + [Fact] + public void DeterminePublishLocationForSingleFileWithDifferentConfigurations() + { + string fullPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fullPath = @"C:\functions\myfunction.cs"; + } + else + { + fullPath = @"/functions/myfunction.cs"; + } + + // Configuration parameter doesn't affect single file publish location (unlike regular projects) + var releaseLocation = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, fullPath, "Release", "net10.0"); + var debugLocation = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, fullPath, "Debug", "net10.0"); + + // Both should point to the same artifacts folder since configuration is not part of the path for single files + Assert.Equal(releaseLocation, debugLocation); + } + + [Fact] + public void DeterminePublishLocationForSingleFileWithDifferentTargetFrameworks() + { + string fullPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fullPath = @"C:\functions\myfunction.cs"; + } + else + { + fullPath = @"/functions/myfunction.cs"; + } + + // Target framework parameter doesn't affect single file publish location (unlike regular projects) + var net10Location = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, fullPath, "Release", "net10.0"); + var net9Location = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, fullPath, "Release", "net9.0"); + + // Both should point to the same artifacts folder since target framework is not part of the path for single files + Assert.Equal(net10Location, net9Location); + } + + [Fact] + public void DeterminePublishLocationForSingleFileVsRegularProject() + { + var assembly = this.GetType().GetTypeInfo().Assembly; + + // Test with single file - should use artifacts folder + var singleFilePath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + var singleFilePublishLocation = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, singleFilePath, "Release", "net10.0"); + + // Single file should have "artifacts" in the path + Assert.Contains("artifacts", singleFilePublishLocation); + Assert.Contains("ToUpperFunctionNoAOT", singleFilePublishLocation); + + // Test with regular project - should use bin/Release/targetFramework/publish + var regularProjectPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/TestFunction"); + var regularProjectPublishLocation = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, regularProjectPath, "Release", "net6.0"); + + // Regular project should have "bin" and "publish" in the path + Assert.Contains("bin", regularProjectPublishLocation); + Assert.Contains("publish", regularProjectPublishLocation); + Assert.Contains("Release", regularProjectPublishLocation); + Assert.Contains("net6.0", regularProjectPublishLocation); + } + + [Fact] + public void DeterminePublishLocationForSingleFileWithSpecialCharacters() + { + string fullPath; + string expectedPublishLocation; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fullPath = @"C:\functions\my-function_v2.cs"; + expectedPublishLocation = @"C:/functions/artifacts/my-function_v2"; + } + else + { + fullPath = @"/functions/my-function_v2.cs"; + expectedPublishLocation = @"/functions/artifacts/my-function_v2"; + } + + var publishLocation = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, fullPath, "Release", "net10.0"); + Assert.Equal(expectedPublishLocation, publishLocation.Replace("\\", "/")); + } + + [Theory] + [InlineData("ToUpperFunctionNoAOT.cs", true)] + [InlineData("ToUpperFunctionNoAOT.vb", false)] + [InlineData("ToUpperFunctionNoAOT.csproj", false)] + [InlineData("ToUpperFunctionNoAOT", false)] + public void ConfirmSingleFileFlagSet(string filename, bool isSingleFile) + { + Assert.Equal(isSingleFile, Utilities.IsSingleFileCSharpFile(filename)); + } + + [Fact] + public void GetSolutionDirectoryForSingleFile() + { + string projectLocation; + string expectedSolutionDirectory; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + projectLocation = @"C:\functions\myfunction.cs"; + expectedSolutionDirectory = @"C:\functions"; + } + else + { + projectLocation = @"/functions/myfunction.cs"; + expectedSolutionDirectory = @"/functions"; + } + + var solutionDirectory = Utilities.GetSolutionDirectoryFullPath(Environment.CurrentDirectory, projectLocation, null); + Assert.Equal(expectedSolutionDirectory, solutionDirectory); + } + + [Fact] + public void GetSolutionDirectoryForSingleFileWithMixedSlashes() + { + string projectLocation; + string expectedSolutionDirectory; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + projectLocation = @"C:\functions/subfolder\myfunction.cs"; + expectedSolutionDirectory = @"C:\functions\subfolder"; + } + else + { + projectLocation = @"/functions/subfolder/myfunction.cs"; + expectedSolutionDirectory = @"/functions/subfolder"; + } + + var solutionDirectory = Utilities.GetSolutionDirectoryFullPath(Environment.CurrentDirectory, projectLocation, null); + Assert.Equal(expectedSolutionDirectory, solutionDirectory); + } + + [Fact] + public void GetSolutionDirectoryForSingleFileVsRegularProject() + { + var assembly = this.GetType().GetTypeInfo().Assembly; + + // Test with single file - should return parent directory + var singleFilePath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + var singleFileSolutionDir = Utilities.GetSolutionDirectoryFullPath(Environment.CurrentDirectory, singleFilePath, null); + var expectedSingleFileDir = Path.GetDirectoryName(singleFilePath); + Assert.Equal(expectedSingleFileDir, singleFileSolutionDir); + + // Test with regular project - should search up for solution file + var regularProjectPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/TestFunction"); + var regularProjectSolutionDir = Utilities.GetSolutionDirectoryFullPath(Environment.CurrentDirectory, regularProjectPath, null); + // For regular projects, it searches up the directory tree, so it should be different behavior + Assert.NotNull(regularProjectSolutionDir); + } + + [Fact] + public void GetSolutionDirectoryWithExplicitSolutionDirectory() + { + string projectLocation; + string givenSolutionDirectory; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + projectLocation = @"C:\functions\myfunction.cs"; + givenSolutionDirectory = @"C:\mysolution"; + } + else + { + projectLocation = @"/functions/myfunction.cs"; + givenSolutionDirectory = @"/mysolution"; + } + + // When an explicit solution directory is provided, it should use that regardless of single file + var solutionDirectory = Utilities.GetSolutionDirectoryFullPath(Environment.CurrentDirectory, projectLocation, givenSolutionDirectory); + Assert.Equal(givenSolutionDirectory, solutionDirectory); + } + } +} diff --git a/test/Amazon.Lambda.Tools.Test/UtilitiesTests.cs b/test/Amazon.Lambda.Tools.Test/UtilitiesTests.cs index 843b7d92..673c14db 100644 --- a/test/Amazon.Lambda.Tools.Test/UtilitiesTests.cs +++ b/test/Amazon.Lambda.Tools.Test/UtilitiesTests.cs @@ -179,6 +179,8 @@ public void TestSavingDockerfileInDefaults(string dockerfilePath, string project [InlineData("net8.0", LambdaConstants.ARCHITECTURE_X86_64, "public.ecr.aws/sam/build-dotnet8:latest-x86_64")] [InlineData("net9.0", LambdaConstants.ARCHITECTURE_ARM64, "public.ecr.aws/sam/build-dotnet9:latest-arm64")] [InlineData("net9.0", LambdaConstants.ARCHITECTURE_X86_64, "public.ecr.aws/sam/build-dotnet9:latest-x86_64")] + [InlineData("net10.0", LambdaConstants.ARCHITECTURE_ARM64, "mcr.microsoft.com/dotnet/sdk:10.0-aot")] + [InlineData("net10.0", LambdaConstants.ARCHITECTURE_X86_64, "mcr.microsoft.com/dotnet/sdk:10.0-aot")] [InlineData(null, LambdaConstants.ARCHITECTURE_X86_64, "throws")] [InlineData(null, LambdaConstants.ARCHITECTURE_ARM64, "throws")] public void GetDefaultBuildImage(string targetFramework, string architecture, string expectedValue) diff --git a/testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs b/testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs new file mode 100644 index 00000000..ff3d7564 --- /dev/null +++ b/testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs @@ -0,0 +1,27 @@ +#:package Amazon.Lambda.Core@2.8.0 +#:package Amazon.Lambda.RuntimeSupport@1.14.1 +#:package Amazon.Lambda.Serialization.SystemTextJson@2.4.4 + +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using System.Text.Json.Serialization; + +// The function handler that will be called for each Lambda event +var handler = (string input, ILambdaContext context) => +{ + return input.ToUpper(); +}; + +// Build the Lambda runtime client passing in the handler to call for each +// event and the JSON serializer to use for translating Lambda JSON documents +// to .NET types. +await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + + +[JsonSerializable(typeof(string))] +public partial class LambdaSerializerContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs b/testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs new file mode 100644 index 00000000..18ae9065 --- /dev/null +++ b/testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs @@ -0,0 +1,28 @@ +#:package Amazon.Lambda.Core@2.8.0 +#:package Amazon.Lambda.RuntimeSupport@1.14.1 +#:package Amazon.Lambda.Serialization.SystemTextJson@2.4.4 +#:property PublishAot=false + +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using System.Text.Json.Serialization; + +// The function handler that will be called for each Lambda event +var handler = (string input, ILambdaContext context) => +{ + return input.ToUpper(); +}; + +// Build the Lambda runtime client passing in the handler to call for each +// event and the JSON serializer to use for translating Lambda JSON documents +// to .NET types. +await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + + +[JsonSerializable(typeof(string))] +public partial class LambdaSerializerContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/testapps/SingeFileLambdaFunctions/serverless.template b/testapps/SingeFileLambdaFunctions/serverless.template new file mode 100644 index 00000000..42213f9d --- /dev/null +++ b/testapps/SingeFileLambdaFunctions/serverless.template @@ -0,0 +1,23 @@ +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Transform" : "AWS::Serverless-2016-10-31", + "Description" : "Starting template for an AWS Serverless Application.", + "Parameters" : { + }, + "Resources" : { + "ToUpperFunctionNoAOT" : { + "Type" : "AWS::Serverless::Function", + "Properties": { + "Handler": "ToUpperFunctionNoAOT", + "Runtime": "dotnet10", + "CodeUri": "./ToUpperFunctionNoAOT.cs", + "Description": "Default function", + "MemorySize": 256, + "Timeout": 30, + "Policies": [ "AWSLambda_FullAccess" ] + } + } + }, + "Outputs" : { + } +} \ No newline at end of file From d46e49b04939ae4d4a7679d49ba9dd1de88c1fd1 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 4 Dec 2025 17:38:20 -0800 Subject: [PATCH 02/11] Add change file --- .../changes/b30ca600-2c50-4ed1-91d0-610f711b9d88.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .autover/changes/b30ca600-2c50-4ed1-91d0-610f711b9d88.json diff --git a/.autover/changes/b30ca600-2c50-4ed1-91d0-610f711b9d88.json b/.autover/changes/b30ca600-2c50-4ed1-91d0-610f711b9d88.json new file mode 100644 index 00000000..bbf26001 --- /dev/null +++ b/.autover/changes/b30ca600-2c50-4ed1-91d0-610f711b9d88.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.Tools", + "Type": "Minor", + "ChangelogMessages": [ + "Add support for packaging and deploying C# file-based Lambda functions" + ] + } + ] +} \ No newline at end of file From 619f88536d9b3b5ce9a12dd35407806f9ffea1a8 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 4 Dec 2025 23:30:41 -0800 Subject: [PATCH 03/11] Fix default package zip file when packaging file-baded function --- src/Amazon.Lambda.Tools/LambdaPackager.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Amazon.Lambda.Tools/LambdaPackager.cs b/src/Amazon.Lambda.Tools/LambdaPackager.cs index 0fa8620a..583afb07 100644 --- a/src/Amazon.Lambda.Tools/LambdaPackager.cs +++ b/src/Amazon.Lambda.Tools/LambdaPackager.cs @@ -237,22 +237,30 @@ public static bool CreateApplicationBundle(LambdaToolsDefaults defaults, IToolLo // If there is no target node then this means the tool is being used on a future version of .NET Core // then was available when the this tool was written. Go ahead and continue the deployment with warnings so the // user can see if the future version will work. - if (depsJsonTargetNode.HasValue && string.Equals(targetFramework, "netcoreapp1.0", StringComparison.OrdinalIgnoreCase)) + if (depsJsonTargetNode.HasValue && string.Equals(targetFramework, "netcoreapp1.0", StringComparison.OrdinalIgnoreCase)) { // Make sure the project is not pulling in dependencies requiring a later version of .NET Core then the declared target framework - if (!ValidateDependencies(logger, targetFramework, depsJsonTargetNode.Value, disableVersionCheck)) + if (!ValidateDependencies(logger, targetFramework, depsJsonTargetNode.Value, disableVersionCheck)) return false; // Flatten the runtime folder which reduces the package size by not including native dependencies // for other platforms. - flattenRuntime = FlattenRuntimeFolder(logger, publishLocation, depsJsonTargetNode.Value); + flattenRuntime = FlattenRuntimeFolder(logger, publishLocation, depsJsonTargetNode.Value); } } FlattenPowerShellRuntimeModules(logger, publishLocation, targetFramework); if (zipArchivePath == null) - zipArchivePath = Path.Combine(Directory.GetParent(publishLocation).FullName, new DirectoryInfo(projectLocation).Name + ".zip"); + { + string baseName; + if (Utilities.IsSingleFileCSharpFile(projectLocation)) + baseName = Path.GetFileNameWithoutExtension(projectLocation); + else + baseName = new DirectoryInfo(projectLocation).Name; + + zipArchivePath = Path.Combine(Directory.GetParent(publishLocation).FullName, baseName + ".zip"); + } zipArchivePath = Path.GetFullPath(zipArchivePath); logger?.WriteLine($"Zipping publish folder {publishLocation} to {zipArchivePath}"); From a6849cdea7c7f219d1b975ecf4530d50d3368994 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 5 Dec 2025 00:15:08 -0800 Subject: [PATCH 04/11] Look for TargetFramework being set in C# file --- .../Commands/DeployFunctionCommand.cs | 2 +- .../Commands/PackageCommand.cs | 2 +- src/Amazon.Lambda.Tools/LambdaUtilities.cs | 61 ++++++++++++++----- .../LambdaSingleFilePackageTests.cs | 48 +++++++++++++++ 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/src/Amazon.Lambda.Tools/Commands/DeployFunctionCommand.cs b/src/Amazon.Lambda.Tools/Commands/DeployFunctionCommand.cs index 8ae281cd..db708a79 100644 --- a/src/Amazon.Lambda.Tools/Commands/DeployFunctionCommand.cs +++ b/src/Amazon.Lambda.Tools/Commands/DeployFunctionCommand.cs @@ -257,7 +257,7 @@ protected override async Task PerformActionAsync() { if (Utilities.IsSingleFileCSharpFile(projectLocation)) { - targetFramework = LambdaUtilities.DetermineTargetFrameworkForSingleFile(lambdaRuntime); + targetFramework = LambdaUtilities.DetermineTargetFrameworkForSingleFile(projectLocation, lambdaRuntime); } else { diff --git a/src/Amazon.Lambda.Tools/Commands/PackageCommand.cs b/src/Amazon.Lambda.Tools/Commands/PackageCommand.cs index dd9b1f56..e47aebae 100644 --- a/src/Amazon.Lambda.Tools/Commands/PackageCommand.cs +++ b/src/Amazon.Lambda.Tools/Commands/PackageCommand.cs @@ -233,7 +233,7 @@ protected override async Task PerformActionAsync() { if (Utilities.IsSingleFileCSharpFile(projectLocation)) { - targetFramework = LambdaUtilities.DetermineTargetFrameworkForSingleFile(null); + targetFramework = LambdaUtilities.DetermineTargetFrameworkForSingleFile(projectLocation, null); } else { diff --git a/src/Amazon.Lambda.Tools/LambdaUtilities.cs b/src/Amazon.Lambda.Tools/LambdaUtilities.cs index 81416b8e..48e65210 100644 --- a/src/Amazon.Lambda.Tools/LambdaUtilities.cs +++ b/src/Amazon.Lambda.Tools/LambdaUtilities.cs @@ -1,22 +1,22 @@ -using System; +using Amazon.Common.DotNetCli.Tools; +using Amazon.Lambda.Model; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.SecurityToken; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text.Json; -using YamlDotNet.RepresentationModel; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Xml.Linq; -using Amazon.Common.DotNetCli.Tools; - -using Amazon.Lambda.Model; -using Amazon.S3; -using Amazon.S3.Model; +using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Xml.Linq; using System.Xml.XPath; +using YamlDotNet.RepresentationModel; using Environment = System.Environment; -using Amazon.SecurityToken; -using System.Runtime.InteropServices; namespace Amazon.Lambda.Tools { @@ -83,16 +83,45 @@ public static string DetermineLambdaRuntimeFromTargetFramework(string targetFram return kvp.Key; } - public static string DetermineTargetFrameworkForSingleFile(string lambdaRuntime) + public static string DetermineTargetFrameworkForSingleFile(string filePath, string lambdaRuntime) { - string targetFramework; + return DetermineTargetFrameworkForSingleFile(File.ReadAllLines(filePath), lambdaRuntime); + } + + public static string DetermineTargetFrameworkForSingleFile(string[] lines, string lambdaRuntime) + { + string targetFramework = null; if (lambdaRuntime != null && _lambdaRuntimeToDotnetFramework.TryGetValue(lambdaRuntime, out targetFramework)) { return targetFramework; } - // TODO: Figure out what to do when we don't have a lambdaRuntime to base it on. - targetFramework = "net10.0"; + // Look for an in-file directive that starts with "#:property" and contains "TargetFramework=" + // Allow extra spaces but the directive must begin with "#:property". Extract the value after '='. + if (lines != null && lines.Length > 0) + { + // Pattern explanation: + // ^\s* -> optional leading whitespace + // #\s*:\s*property -> literal "#", optional spaces, ":", optional spaces, "property" + // \s+ -> at least one space (separates property from key) + // TargetFramework -> literal key + // \s*=\s* -> equals with optional surrounding spaces + // (\S+) -> capture the value (non-whitespace sequence) + var directiveRegex = new Regex(@"^\s*#\s*:\s*property\s+TargetFramework\s*=\s*(\S+)", RegexOptions.IgnoreCase); + + foreach (var rawLine in lines) + { + if (string.IsNullOrWhiteSpace(rawLine)) + continue; + + var m = directiveRegex.Match(rawLine); + if (m.Success && m.Groups.Count > 1) + { + targetFramework = m.Groups[1].Value.Trim(); + break; + } + } + } return targetFramework; } diff --git a/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs b/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs index 22212054..f7589609 100644 --- a/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs +++ b/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs @@ -298,5 +298,53 @@ public void GetSolutionDirectoryWithExplicitSolutionDirectory() var solutionDirectory = Utilities.GetSolutionDirectoryFullPath(Environment.CurrentDirectory, projectLocation, givenSolutionDirectory); Assert.Equal(givenSolutionDirectory, solutionDirectory); } + + [Fact] + public void DetermineTargetFrameworkForSingleFile_FindsExactDirective() + { + var lines = new[] { "#:property TargetFramework=net10.0" }; + var result = LambdaUtilities.DetermineTargetFrameworkForSingleFile(lines, null); + Assert.Equal("net10.0", result); + } + + [Fact] + public void DetermineTargetFrameworkForSingleFile_FindsDirectiveWithExtraSpaces() + { + var lines = new[] { "#:property TargetFramework=net11.0" }; + var result = LambdaUtilities.DetermineTargetFrameworkForSingleFile(lines, null); + Assert.Equal("net11.0", result); + } + + [Fact] + public void DetermineTargetFrameworkForSingleFile_FindsDirectiveWithSpacesAroundEquals() + { + var lines = new[] { "#:property TargetFramework = net12.0" }; + var result = LambdaUtilities.DetermineTargetFrameworkForSingleFile(lines, null); + Assert.Equal("net12.0", result); + } + + [Fact] + public void DetermineTargetFrameworkForSingleFile_CommentedOut() + { + var lines = new[] { "//#:property TargetFramework=NET13.0" }; + var result = LambdaUtilities.DetermineTargetFrameworkForSingleFile(lines, null); + Assert.Null(result); + } + + [Fact] + public void DetermineTargetFrameworkForSingleFile_WithLambdaRuntime() + { + var lines = new[] { "// Nothing to see here" }; + var result = LambdaUtilities.DetermineTargetFrameworkForSingleFile(lines, "dotnet10"); + Assert.Equal("net10.0", result); + } + + [Fact] + public void DetermineTargetFrameworkForSingleFile_NoDirectiveReturnsNull() + { + var lines = new[] { "// just a comment", "using System;" }; + var result = LambdaUtilities.DetermineTargetFrameworkForSingleFile(lines, null); + Assert.Null(result); + } } } From 6f1065c86002394587cba705383de1463b734dd2 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 5 Dec 2025 09:57:00 -0800 Subject: [PATCH 05/11] Add .NET 10 to CI CodeBuild buildspec --- buildtools/ci.buildspec.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/buildtools/ci.buildspec.yml b/buildtools/ci.buildspec.yml index 7591a753..844a7cfd 100644 --- a/buildtools/ci.buildspec.yml +++ b/buildtools/ci.buildspec.yml @@ -7,7 +7,9 @@ phases: install: runtime-versions: dotnet: 8.x - build: + commands: + - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 + - dotnet --version commands: - dotnet test --verbosity normal test/Amazon.Common.DotNetCli.Tools.Test/Amazon.Common.DotNetCli.Tools.Test.csproj --configuration Release --logger trx --results-directory ./testresults - dotnet test --verbosity normal test/Amazon.ECS.Tools.Test/Amazon.ECS.Tools.Test.csproj --configuration Release --logger trx --results-directory ./testresults From 8fb1950f15e8dd5c98dabc09254bd350b6809d4e Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 5 Dec 2025 11:41:40 -0800 Subject: [PATCH 06/11] Fix syntax issue --- buildtools/ci.buildspec.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/buildtools/ci.buildspec.yml b/buildtools/ci.buildspec.yml index 844a7cfd..fbc8bbca 100644 --- a/buildtools/ci.buildspec.yml +++ b/buildtools/ci.buildspec.yml @@ -10,6 +10,7 @@ phases: commands: - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 - dotnet --version + build: commands: - dotnet test --verbosity normal test/Amazon.Common.DotNetCli.Tools.Test/Amazon.Common.DotNetCli.Tools.Test.csproj --configuration Release --logger trx --results-directory ./testresults - dotnet test --verbosity normal test/Amazon.ECS.Tools.Test/Amazon.ECS.Tools.Test.csproj --configuration Release --logger trx --results-directory ./testresults From f009b8ea7a3a0ccb493a834b1efbd42429f533fc Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 5 Dec 2025 13:28:08 -0800 Subject: [PATCH 07/11] Fix unit tests --- testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs | 1 + testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs b/testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs index ff3d7564..a5d1f883 100644 --- a/testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs +++ b/testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs @@ -1,6 +1,7 @@ #:package Amazon.Lambda.Core@2.8.0 #:package Amazon.Lambda.RuntimeSupport@1.14.1 #:package Amazon.Lambda.Serialization.SystemTextJson@2.4.4 +#:property TargetFramework=net10.0 using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; diff --git a/testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs b/testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs index 18ae9065..6f8bc892 100644 --- a/testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs +++ b/testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs @@ -1,6 +1,7 @@ #:package Amazon.Lambda.Core@2.8.0 #:package Amazon.Lambda.RuntimeSupport@1.14.1 #:package Amazon.Lambda.Serialization.SystemTextJson@2.4.4 +#:property TargetFramework=net10.0 #:property PublishAot=false using Amazon.Lambda.Core; From 585c22fafa54098b485de7a8bd111f829d300fb5 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 5 Dec 2025 13:44:35 -0800 Subject: [PATCH 08/11] Address PR comments. --- aws-extensions-for-dotnet-cli.sln | 8 +-- .../Utilities.cs | 6 +- .../Amazon.Lambda.Tools.csproj | 2 +- src/Amazon.Lambda.Tools/LambdaPackager.cs | 66 +++++++++---------- .../DeployProjectTests.cs | 2 +- .../DeployServerlessTests.cs | 2 +- .../LambdaSingleFilePackageTests.cs | 10 +-- .../ToUpperFunctionImplicitAOT.cs | 0 .../ToUpperFunctionNoAOT.cs | 0 .../serverless.template | 2 +- 10 files changed, 49 insertions(+), 49 deletions(-) rename testapps/{SingeFileLambdaFunctions => SingleFileLambdaFunctions}/ToUpperFunctionImplicitAOT.cs (100%) rename testapps/{SingeFileLambdaFunctions => SingleFileLambdaFunctions}/ToUpperFunctionNoAOT.cs (100%) rename testapps/{SingeFileLambdaFunctions => SingleFileLambdaFunctions}/serverless.template (91%) diff --git a/aws-extensions-for-dotnet-cli.sln b/aws-extensions-for-dotnet-cli.sln index d787390d..449f0585 100644 --- a/aws-extensions-for-dotnet-cli.sln +++ b/aws-extensions-for-dotnet-cli.sln @@ -69,11 +69,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestIntegerFunction", "test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFunctionBuildProps", "testapps\TestFunctionBuildProps\TestFunctionBuildProps\TestFunctionBuildProps.csproj", "{AFA71B8E-F0AA-4704-8C4E-C11130F82B13}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SingeFileLambdaFunctions", "SingeFileLambdaFunctions", "{5711F48F-5491-4C8A-92B2-4D6D849483B5}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SingleFileLambdaFunctions", "SingleFileLambdaFunctions", "{5711F48F-5491-4C8A-92B2-4D6D849483B5}" ProjectSection(SolutionItems) = preProject - testapps\SingeFileLambdaFunctions\serverless.template = testapps\SingeFileLambdaFunctions\serverless.template - testapps\SingeFileLambdaFunctions\ToUpperFunctionImplicitAOT.cs = testapps\SingeFileLambdaFunctions\ToUpperFunctionImplicitAOT.cs - testapps\SingeFileLambdaFunctions\ToUpperFunctionNoAOT.cs = testapps\SingeFileLambdaFunctions\ToUpperFunctionNoAOT.cs + testapps\SingleFileLambdaFunctions\serverless.template = testapps\SingleFileLambdaFunctions\serverless.template + testapps\SingleFileLambdaFunctions\ToUpperFunctionImplicitAOT.cs = testapps\SingleFileLambdaFunctions\ToUpperFunctionImplicitAOT.cs + testapps\SingleFileLambdaFunctions\ToUpperFunctionNoAOT.cs = testapps\SingleFileLambdaFunctions\ToUpperFunctionNoAOT.cs EndProjectSection EndProject Global diff --git a/src/Amazon.Common.DotNetCli.Tools/Utilities.cs b/src/Amazon.Common.DotNetCli.Tools/Utilities.cs index 34985471..263f45e0 100644 --- a/src/Amazon.Common.DotNetCli.Tools/Utilities.cs +++ b/src/Amazon.Common.DotNetCli.Tools/Utilities.cs @@ -209,12 +209,12 @@ public static string GetSolutionDirectoryFullPath(string workingDirectory, strin public static string DeterminePublishLocation(string workingDirectory, string projectLocation, string configuration, string targetFramework) { string path; - if (IsSingleFileCSharpFile(projectLocation)) + if (IsSingleFileCSharpFile(projectLocation)) // For example projectLocation = /path/to/file.cs { // Do not rely on Directory.GetParent() because if this value will eventually run inside a container but // the host is Windows the slashes and root will get messed up. - int forwardSlashPosition = projectLocation.LastIndexOf('\\'); - int backSlashPosition = projectLocation.LastIndexOf('/'); + int forwardSlashPosition = projectLocation.LastIndexOf('/'); + int backSlashPosition = projectLocation.LastIndexOf('\\'); int position = Math.Max(forwardSlashPosition, backSlashPosition); var parentFolder = projectLocation.Substring(0, position); diff --git a/src/Amazon.Lambda.Tools/Amazon.Lambda.Tools.csproj b/src/Amazon.Lambda.Tools/Amazon.Lambda.Tools.csproj index 11bfcbc7..0ceff410 100644 --- a/src/Amazon.Lambda.Tools/Amazon.Lambda.Tools.csproj +++ b/src/Amazon.Lambda.Tools/Amazon.Lambda.Tools.csproj @@ -20,7 +20,7 @@ the source code here is also copied into the AWS VS Toolkit. --> 7.3 - 5.13.2 + 5.13.888 diff --git a/src/Amazon.Lambda.Tools/LambdaPackager.cs b/src/Amazon.Lambda.Tools/LambdaPackager.cs index 583afb07..537f14a8 100644 --- a/src/Amazon.Lambda.Tools/LambdaPackager.cs +++ b/src/Amazon.Lambda.Tools/LambdaPackager.cs @@ -378,33 +378,33 @@ public static IDictionary ConvertToMapOfFiles(string rootDirecto { if (!doc.RootElement.TryGetProperty("runtimeTarget", out JsonElement runtimeTargetNode)) { - logger?.WriteLine($"Missing runtimeTarget node. Skipping flattening runtime folder because {depsJsonFilepath} is an unrecognized format"); - return null; - } + logger?.WriteLine($"Missing runtimeTarget node. Skipping flattening runtime folder because {depsJsonFilepath} is an unrecognized format"); + return null; + } - string runtimeTarget; + string runtimeTarget; if (runtimeTargetNode.ValueKind == JsonValueKind.String) - { + { runtimeTarget = runtimeTargetNode.GetString(); - } + } else if (runtimeTargetNode.TryGetProperty("name", out JsonElement nameElement)) - { + { runtimeTarget = nameElement.GetString(); - } + } else - { - logger?.WriteLine($"Missing runtimeTarget name. Skipping flattening runtime folder because {depsJsonFilepath} is an unrecognized format"); - return null; - } + { + logger?.WriteLine($"Missing runtimeTarget name. Skipping flattening runtime folder because {depsJsonFilepath} is an unrecognized format"); + return null; + } if (!doc.RootElement.TryGetProperty("targets", out JsonElement targets) || !targets.TryGetProperty(runtimeTarget, out JsonElement target)) - { - logger?.WriteLine($"Missing targets node. Skipping flattening runtime folder because {depsJsonFilepath} is an unrecognized format"); - return null; - } + { + logger?.WriteLine($"Missing targets node. Skipping flattening runtime folder because {depsJsonFilepath} is an unrecognized format"); + return null; + } return target.Clone(); - } + } } catch (Exception e) { @@ -571,14 +571,14 @@ private static bool FlattenRuntimeFolder(IToolLogger logger, string publishLocat if (depRuntimeTarget.Value.TryGetProperty("rid", out JsonElement ridElement)) { var rid = ridElement.GetString(); - if(string.Equals(rid, runtime, StringComparison.Ordinal)) - { + if(string.Equals(rid, runtime, StringComparison.Ordinal)) + { copyFileIfNotExist(depRuntimeTarget.Name); + } } } } } - } return true; } @@ -604,29 +604,29 @@ private static IList CalculateRuntimeHierarchy() if (!doc.RootElement.TryGetProperty("runtimes", out JsonElement runtimes)) return runtimeHierarchy; - // Use a queue to do a breadth first search through the list of runtimes. - var queue = new Queue(); - queue.Enqueue(LambdaConstants.LEGACY_RUNTIME_HIERARCHY_STARTING_POINT); + // Use a queue to do a breadth first search through the list of runtimes. + var queue = new Queue(); + queue.Enqueue(LambdaConstants.LEGACY_RUNTIME_HIERARCHY_STARTING_POINT); - while(queue.Count > 0) - { - var runtime = queue.Dequeue(); - if (runtimeHierarchy.Contains(runtime)) - continue; + while(queue.Count > 0) + { + var runtime = queue.Dequeue(); + if (runtimeHierarchy.Contains(runtime)) + continue; - runtimeHierarchy.Add(runtime); + runtimeHierarchy.Add(runtime); if (runtimes.TryGetProperty(runtime, out JsonElement runtimeElement) && runtimeElement.TryGetProperty("#import", out JsonElement imports)) - { - foreach (JsonElement importedRuntime in imports.EnumerateArray()) { - queue.Enqueue(importedRuntime.GetString()); + foreach (JsonElement importedRuntime in imports.EnumerateArray()) + { + queue.Enqueue(importedRuntime.GetString()); + } } } } } - } return runtimeHierarchy; } diff --git a/test/Amazon.Lambda.Tools.Integ.Tests/DeployProjectTests.cs b/test/Amazon.Lambda.Tools.Integ.Tests/DeployProjectTests.cs index 547c8842..74e6d80b 100644 --- a/test/Amazon.Lambda.Tools.Integ.Tests/DeployProjectTests.cs +++ b/test/Amazon.Lambda.Tools.Integ.Tests/DeployProjectTests.cs @@ -340,7 +340,7 @@ public async Task TestDeploySingleCSharpFile() { var assembly = this.GetType().GetTypeInfo().Assembly; var toolLogger = new TestToolLogger(_testOutputHelper); - var csharpFile = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + var csharpFile = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingleFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); var functionName = "TestDeploySingleCSharpFile-" + DateTime.Now.Ticks; var deployFunctionCommand = new DeployFunctionCommand(toolLogger, System.Environment.CurrentDirectory, new string[] { csharpFile }); diff --git a/test/Amazon.Lambda.Tools.Integ.Tests/DeployServerlessTests.cs b/test/Amazon.Lambda.Tools.Integ.Tests/DeployServerlessTests.cs index 242d545c..b9da43f5 100644 --- a/test/Amazon.Lambda.Tools.Integ.Tests/DeployServerlessTests.cs +++ b/test/Amazon.Lambda.Tools.Integ.Tests/DeployServerlessTests.cs @@ -286,7 +286,7 @@ public async Task TestDeployServerlessReferencingSingleFile() { var assembly = this.GetType().GetTypeInfo().Assembly; var toolLogger = new TestToolLogger(_testOutputHelper); - var templatePath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/serverless.template"); + var templatePath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingleFileLambdaFunctions/serverless.template"); var stackName = "TestDeployServerlessReferencingSingleFile-" + DateTime.Now.Ticks; var deployServerlessCommand = new DeployServerlessCommand(toolLogger, Environment.CurrentDirectory, new string[] { "--template", templatePath }); diff --git a/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs b/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs index f7589609..50e5ddfe 100644 --- a/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs +++ b/test/Amazon.Lambda.Tools.Test/LambdaSingleFilePackageTests.cs @@ -27,7 +27,7 @@ public async Task PackageToUpperNoAOTSettingWithArgument() try { var assembly = this.GetType().GetTypeInfo().Assembly; - var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingleFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); var command = new PackageCommand(new TestToolLogger(_testOutputHelper), Environment.CurrentDirectory, new string[] {fullPath, tempFile }); var created = await command.ExecuteAsync(); @@ -52,7 +52,7 @@ public async Task PackageToUpperNoAOTSettingWithProjectLocation() try { var assembly = this.GetType().GetTypeInfo().Assembly; - var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingleFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); var command = new PackageCommand(new TestToolLogger(_testOutputHelper), Environment.CurrentDirectory, new string[] { tempFile, "--project-location", fullPath }); var created = await command.ExecuteAsync(); @@ -78,7 +78,7 @@ public async Task PackageToUpperNoAOTSettingWithProjectLocation() public void ConfirmUsingNativeAOT(string filename, bool isAot, string msBuildParameters) { var assembly = this.GetType().GetTypeInfo().Assembly; - var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + $"../../../../../../testapps/SingeFileLambdaFunctions/{filename}"); + var fullPath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + $"../../../../../../testapps/SingleFileLambdaFunctions/{filename}"); var actualAot = Utilities.LookPublishAotFlag(fullPath, msBuildParameters); Assert.Equal(isAot, actualAot); @@ -172,7 +172,7 @@ public void DeterminePublishLocationForSingleFileVsRegularProject() var assembly = this.GetType().GetTypeInfo().Assembly; // Test with single file - should use artifacts folder - var singleFilePath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + var singleFilePath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingleFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); var singleFilePublishLocation = Utilities.DeterminePublishLocation(Environment.CurrentDirectory, singleFilePath, "Release", "net10.0"); // Single file should have "artifacts" in the path @@ -266,7 +266,7 @@ public void GetSolutionDirectoryForSingleFileVsRegularProject() var assembly = this.GetType().GetTypeInfo().Assembly; // Test with single file - should return parent directory - var singleFilePath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); + var singleFilePath = Path.GetFullPath(Path.GetDirectoryName(assembly.Location) + "../../../../../../testapps/SingleFileLambdaFunctions/ToUpperFunctionNoAOT.cs"); var singleFileSolutionDir = Utilities.GetSolutionDirectoryFullPath(Environment.CurrentDirectory, singleFilePath, null); var expectedSingleFileDir = Path.GetDirectoryName(singleFilePath); Assert.Equal(expectedSingleFileDir, singleFileSolutionDir); diff --git a/testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs b/testapps/SingleFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs similarity index 100% rename from testapps/SingeFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs rename to testapps/SingleFileLambdaFunctions/ToUpperFunctionImplicitAOT.cs diff --git a/testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs b/testapps/SingleFileLambdaFunctions/ToUpperFunctionNoAOT.cs similarity index 100% rename from testapps/SingeFileLambdaFunctions/ToUpperFunctionNoAOT.cs rename to testapps/SingleFileLambdaFunctions/ToUpperFunctionNoAOT.cs diff --git a/testapps/SingeFileLambdaFunctions/serverless.template b/testapps/SingleFileLambdaFunctions/serverless.template similarity index 91% rename from testapps/SingeFileLambdaFunctions/serverless.template rename to testapps/SingleFileLambdaFunctions/serverless.template index 42213f9d..0bbbabbe 100644 --- a/testapps/SingeFileLambdaFunctions/serverless.template +++ b/testapps/SingleFileLambdaFunctions/serverless.template @@ -14,7 +14,7 @@ "Description": "Default function", "MemorySize": 256, "Timeout": 30, - "Policies": [ "AWSLambda_FullAccess" ] + "Policies": [ "AWSLambdaBasicExecutionRole " ] } } }, From 9f56aa2ea3c26a15f1a979a929cfcf8af7dc2682 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 5 Dec 2025 13:53:16 -0800 Subject: [PATCH 09/11] Remove extra white space --- testapps/SingleFileLambdaFunctions/serverless.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testapps/SingleFileLambdaFunctions/serverless.template b/testapps/SingleFileLambdaFunctions/serverless.template index 0bbbabbe..a223f6bf 100644 --- a/testapps/SingleFileLambdaFunctions/serverless.template +++ b/testapps/SingleFileLambdaFunctions/serverless.template @@ -14,7 +14,7 @@ "Description": "Default function", "MemorySize": 256, "Timeout": 30, - "Policies": [ "AWSLambdaBasicExecutionRole " ] + "Policies": [ "AWSLambdaBasicExecutionRole" ] } } }, From 30ce54e0b9dfc717499e9b4d81ae3f740ad224ea Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 5 Dec 2025 16:33:53 -0800 Subject: [PATCH 10/11] CI Remove the CodeBuild .NET Install --- buildtools/ci.buildspec.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/buildtools/ci.buildspec.yml b/buildtools/ci.buildspec.yml index fbc8bbca..15fb5cd4 100644 --- a/buildtools/ci.buildspec.yml +++ b/buildtools/ci.buildspec.yml @@ -5,8 +5,6 @@ env: DOTNET_RUNNING_IN_CONTAINER: "true" phases: install: - runtime-versions: - dotnet: 8.x commands: - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 - dotnet --version From b3c27132b0571e294d54e6366fac5b8d3961d904 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 5 Dec 2025 16:38:03 -0800 Subject: [PATCH 11/11] Add Path --- buildtools/ci.buildspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildtools/ci.buildspec.yml b/buildtools/ci.buildspec.yml index 15fb5cd4..c76d6971 100644 --- a/buildtools/ci.buildspec.yml +++ b/buildtools/ci.buildspec.yml @@ -7,7 +7,7 @@ phases: install: commands: - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 - - dotnet --version + - dotnet new globaljson --sdk-version 10.0.0 --roll-forward minor build: commands: - dotnet test --verbosity normal test/Amazon.Common.DotNetCli.Tools.Test/Amazon.Common.DotNetCli.Tools.Test.csproj --configuration Release --logger trx --results-directory ./testresults