Skip to content

Commit c901b8d

Browse files
Merge branch 'develop'
2 parents 71f10c1 + 65ec22d commit c901b8d

File tree

8 files changed

+251
-9
lines changed

8 files changed

+251
-9
lines changed

aws_lambda_builders/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44

55
# Changing version will trigger a new release!
66
# Please make the version change as the last step of your development.
7-
__version__ = "1.21.0"
7+
__version__ = "1.22.0"
88
RPC_PROTOCOL_VERSION = "0.3"

aws_lambda_builders/workflows/dotnet_clipackage/actions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def execute(self):
108108

109109
# The dotnet lambda package command outputs a zip file for the package. To make this compatible
110110
# with the workflow, unzip the zip file into the artifacts directory and then delete the zip archive.
111-
self.os_utils.expand_zip(zipfullpath, self.artifacts_dir)
111+
self.os_utils.unzip(zipfullpath, self.artifacts_dir)
112112

113113
except DotnetCLIExecutionError as ex:
114114
raise ActionFailedError(str(ex))

aws_lambda_builders/workflows/dotnet_clipackage/utils.py

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""
22
Commonly used utilities
33
"""
4-
4+
import logging
55
import os
66
import platform
7-
import shutil
87
import subprocess
98
import zipfile
109
from aws_lambda_builders.utils import which
1110

11+
LOG = logging.getLogger(__name__)
12+
1213

1314
class OSUtils(object):
1415
"""
@@ -25,11 +26,126 @@ def is_windows(self):
2526
def which(self, executable, executable_search_paths=None):
2627
return which(executable, executable_search_paths=executable_search_paths)
2728

28-
def expand_zip(self, zipfullpath, destination_dir):
29-
ziparchive = zipfile.ZipFile(zipfullpath, "r")
30-
ziparchive.extractall(destination_dir)
31-
ziparchive.close()
32-
os.remove(zipfullpath)
29+
def unzip(self, zip_file_path, output_dir, permission=None):
30+
"""
31+
This method and dependent methods were copied from SAM CLI, but with the addition of deleting the zip file
32+
https://github.com/aws/aws-sam-cli/blob/458076265651237a662a372f54d5b3df49fd6797/samcli/local/lambdafn/zip.py#L81
33+
34+
Unzip the given file into the given directory while preserving file permissions in the process.
35+
Parameters
36+
----------
37+
zip_file_path : str
38+
Path to the zip file
39+
output_dir : str
40+
Path to the directory where the it should be unzipped to
41+
permission : int
42+
Permission to set in an octal int form
43+
"""
44+
45+
with zipfile.ZipFile(zip_file_path, "r") as zip_ref:
46+
47+
# For each item in the zip file, extract the file and set permissions if available
48+
for file_info in zip_ref.infolist():
49+
extracted_path = self._extract(file_info, output_dir, zip_ref)
50+
51+
# If the extracted_path is a symlink, do not set the permissions. If the target of the symlink does not
52+
# exist, then os.chmod will fail with FileNotFoundError
53+
if not os.path.islink(extracted_path):
54+
self._set_permissions(file_info, extracted_path)
55+
self._override_permissions(extracted_path, permission)
56+
57+
if not os.path.islink(extracted_path):
58+
self._override_permissions(output_dir, permission)
59+
60+
os.remove(zip_file_path)
61+
62+
def _is_symlink(self, file_info):
63+
"""
64+
Check the upper 4 bits of the external attribute for a symlink.
65+
See: https://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute
66+
Parameters
67+
----------
68+
file_info : zipfile.ZipInfo
69+
The ZipInfo for a ZipFile
70+
Returns
71+
-------
72+
bool
73+
A response regarding whether the ZipInfo defines a symlink or not.
74+
"""
75+
76+
return (file_info.external_attr >> 28) == 0xA
77+
78+
def _extract(self, file_info, output_dir, zip_ref):
79+
"""
80+
Unzip the given file into the given directory while preserving file permissions in the process.
81+
Parameters
82+
----------
83+
file_info : zipfile.ZipInfo
84+
The ZipInfo for a ZipFile
85+
output_dir : str
86+
Path to the directory where the it should be unzipped to
87+
zip_ref : zipfile.ZipFile
88+
The ZipFile we are working with.
89+
Returns
90+
-------
91+
string
92+
Returns the target path the Zip Entry was extracted to.
93+
"""
94+
95+
# Handle any regular file/directory entries
96+
if not self._is_symlink(file_info):
97+
return zip_ref.extract(file_info, output_dir)
98+
99+
source = zip_ref.read(file_info.filename).decode("utf8")
100+
link_name = os.path.normpath(os.path.join(output_dir, file_info.filename))
101+
102+
# make leading dirs if needed
103+
leading_dirs = os.path.dirname(link_name)
104+
if not os.path.exists(leading_dirs):
105+
os.makedirs(leading_dirs)
106+
107+
# If the link already exists, delete it or symlink() fails
108+
if os.path.lexists(link_name):
109+
os.remove(link_name)
110+
111+
# Create a symbolic link pointing to source named link_name.
112+
os.symlink(source, link_name)
113+
114+
return link_name
115+
116+
def _override_permissions(self, path, permission):
117+
"""
118+
Forcefully override the permissions on the path
119+
Parameters
120+
----------
121+
path str
122+
Path where the file or directory
123+
permission octal int
124+
Permission to set
125+
"""
126+
if permission:
127+
os.chmod(path, permission)
128+
129+
def _set_permissions(self, zip_file_info, extracted_path):
130+
"""
131+
Sets permissions on the extracted file by reading the ``external_attr`` property of given file info.
132+
Parameters
133+
----------
134+
zip_file_info : zipfile.ZipInfo
135+
Object containing information about a file within a zip archive
136+
extracted_path : str
137+
Path where the file has been extracted to
138+
"""
139+
140+
# Permission information is stored in first two bytes.
141+
permission = zip_file_info.external_attr >> 16
142+
if not permission:
143+
# Zips created on certain Windows machines, however, might not have any permission information on them.
144+
# Skip setting a permission on these files.
145+
LOG.debug("File %s in zipfile does not have permission information", zip_file_info.filename)
146+
return
147+
148+
os.chmod(extracted_path, permission)
33149

34150
@property
35151
def pipe(self):

tests/integration/workflows/dotnet_clipackage/test_dotnet.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ def verify_architecture(self, deps_file_name, expected_architecture, version=Non
4545

4646
self.assertEqual(target, target_name)
4747

48+
def verify_execute_permissions(self, entrypoint_file_name):
49+
entrypoint_file_path = os.path.join(self.artifacts_dir, entrypoint_file_name)
50+
self.assertTrue(os.access(entrypoint_file_path, os.X_OK))
51+
4852

4953
class TestDotnet31(TestDotnetBase):
5054
"""
@@ -192,3 +196,29 @@ def test_with_defaults_file_arm64(self):
192196

193197
self.assertEqual(expected_files, output_files)
194198
self.verify_architecture("WithDefaultsFile.deps.json", "linux-arm64", version="6.0")
199+
200+
def test_with_custom_runtime(self):
201+
source_dir = os.path.join(self.TEST_DATA_FOLDER, "CustomRuntime6")
202+
203+
self.builder.build(
204+
source_dir, self.artifacts_dir, self.scratch_dir, source_dir, runtime=self.runtime, architecture=X86_64
205+
)
206+
207+
expected_files = {
208+
"Amazon.Lambda.Core.dll",
209+
"Amazon.Lambda.RuntimeSupport.dll",
210+
"Amazon.Lambda.Serialization.SystemTextJson.dll",
211+
"bootstrap",
212+
"bootstrap.deps.json",
213+
"bootstrap.dll",
214+
"bootstrap.pdb",
215+
"bootstrap.runtimeconfig.json",
216+
}
217+
218+
output_files = set(os.listdir(self.artifacts_dir))
219+
220+
self.assertEqual(expected_files, output_files)
221+
self.verify_architecture("bootstrap.deps.json", "linux-x64", version="6.0")
222+
# Execute permissions are required for custom runtimes which bootstrap themselves, otherwise `sam local invoke`
223+
# won't have permission to run the file
224+
self.verify_execute_permissions("bootstrap")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<AWSProjectType>Lambda</AWSProjectType>
8+
<AssemblyName>bootstrap</AssemblyName>
9+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
10+
<PublishReadyToRun>true</PublishReadyToRun>
11+
</PropertyGroup>
12+
<!--
13+
When publishing Lambda functions for ARM64 to the provided.al2 runtime a newer version of libicu needs to be included
14+
in the deployment bundle because .NET requires a newer version of libicu then is preinstalled with Amazon Linux 2.
15+
-->
16+
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-arm64'">
17+
<RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="68.2.0.9" />
18+
<PackageReference Include="Microsoft.ICU.ICU4C.Runtime" Version="68.2.0.9" />
19+
</ItemGroup>
20+
<ItemGroup>
21+
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.8.2" />
22+
<PackageReference Include="Amazon.Lambda.Core" Version="2.1.0" />
23+
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.3.0" />
24+
</ItemGroup>
25+
</Project>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Amazon.Lambda.Core;
2+
using Amazon.Lambda.RuntimeSupport;
3+
using Amazon.Lambda.Serialization.SystemTextJson;
4+
5+
namespace CustomRuntime6;
6+
7+
public class Function
8+
{
9+
private static async Task Main(string[] args)
10+
{
11+
Func<string, ILambdaContext, string> handler = FunctionHandler;
12+
await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer())
13+
.Build()
14+
.RunAsync();
15+
}
16+
17+
public static string FunctionHandler(string input, ILambdaContext context)
18+
{
19+
return input.ToUpper();
20+
}
21+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"Information": [
3+
"This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
4+
"To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
5+
"dotnet lambda help",
6+
"All the command line options for the Lambda command can be specified in this file."
7+
],
8+
"profile": "",
9+
"region": "",
10+
"configuration": "Release",
11+
"function-runtime": "provided.al2",
12+
"function-memory-size": 256,
13+
"function-timeout": 30,
14+
"function-handler": "bootstrap",
15+
"msbuild-parameters": "--self-contained true"
16+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import os
2+
import stat
3+
import tempfile
4+
from unittest import TestCase
5+
from zipfile import ZipFile
6+
7+
from aws_lambda_builders.workflows.dotnet_clipackage.utils import OSUtils
8+
9+
10+
class TestDotnetCliPackageWorkflow(TestCase):
11+
def test_unzip_keeps_execute_permission_on_linux(self):
12+
with tempfile.TemporaryDirectory() as temp_dir:
13+
with tempfile.TemporaryDirectory(dir=temp_dir) as output_dir:
14+
test_file_name = "myFileToZip"
15+
path_to_file_to_zip = os.path.join(temp_dir, test_file_name)
16+
path_to_zip_file = os.path.join(temp_dir, "myZip.zip")
17+
expected_output_file = os.path.join(output_dir, test_file_name)
18+
with open(path_to_file_to_zip, "a") as the_file:
19+
the_file.write("Hello World!")
20+
21+
# Set execute permissions on the file before zipping (won't do anything on Windows)
22+
st = os.stat(path_to_file_to_zip)
23+
os.chmod(path_to_file_to_zip, st.st_mode | stat.S_IEXEC | stat.S_IXOTH | stat.S_IXGRP)
24+
25+
# Zip the file
26+
with ZipFile(path_to_zip_file, "w") as myzip:
27+
myzip.write(path_to_file_to_zip, test_file_name)
28+
29+
# Unzip the file
30+
OSUtils().unzip(path_to_zip_file, output_dir)
31+
self.assertTrue(os.path.exists(expected_output_file))
32+
33+
# Assert that execute permissions are still applied
34+
self.assertTrue(os.access(expected_output_file, os.X_OK))

0 commit comments

Comments
 (0)