239239# the Lambda.
240240# See: https://github.com/Miserlou/Zappa/pull/1730
241241ALB_LAMBDA_ALIAS = "current-alb-version"
242-
243- ##
244- # Classes
245- ##
242+ X86_ARCHITECTURE = "x86_64"
243+ ARM_ARCHITECTURE = "arm64"
244+ VALID_ARCHITECTURES = (X86_ARCHITECTURE , ARM_ARCHITECTURE )
245+
246+
247+ def build_manylinux_wheel_file_match_pattern (runtime : str , architecture : str ) -> re .Pattern :
248+ # Support PEP600 (https://peps.python.org/pep-0600/)
249+ # The wheel filename is {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
250+ runtime_major_version , runtime_minor_version = runtime [6 :].split ("." )
251+ python_tag = f"cp{ runtime_major_version } { runtime_minor_version } " # python3.13 -> cp313
252+ manylinux_legacy_tags = ("manylinux2014" , "manylinux2010" , "manylinux1" )
253+ if architecture == X86_ARCHITECTURE :
254+ valid_platform_tags = [X86_ARCHITECTURE ]
255+ elif architecture == ARM_ARCHITECTURE :
256+ valid_platform_tags = [ARM_ARCHITECTURE , "aarch64" ]
257+ else :
258+ raise ValueError (f"Invalid 'architecture', must be one of { VALID_ARCHITECTURES } , got: { architecture } " )
259+
260+ manylinux_wheel_file_match = (
261+ rf'^.*{ python_tag } -(manylinux_\d+_\d+_({ "|" .join (valid_platform_tags )} )[.])?'
262+ rf'({ "|" .join (manylinux_legacy_tags )} )_({ "|" .join (valid_platform_tags )} )[.]whl$'
263+ )
264+
265+ # The 'abi3' tag is a compiled distribution format designed for compatibility across multiple Python 3 versions.
266+ # An abi3 wheel is built against the stable ABI (Application Binary Interface) of a minimum supported Python version.
267+ # -- make sure cp3XX version is <= to the runtime version (runtime_minor_version)
268+ minimum_minor_version = 5
269+ abi_valid_python_minor_versions = [str (i ) for i in range (minimum_minor_version , int (runtime_minor_version ) + 1 )]
270+ manylinux_suffixes = [r"_\d+_\d+" , r"manylinux_\d+_\d+" ]
271+ manylinux_suffixes .extend (manylinux_legacy_tags )
272+ manylinux_wheel_abi3_file_match = (
273+ # rf'^.*cp3.-abi3-manylinux({"|".join(manylinux_suffixes)})_({"|".join(valid_platform_tags)}).whl$'
274+ rf'^.*cp3({ "|" .join (abi_valid_python_minor_versions )} )-abi3-'
275+ rf'manylinux(({ "|" .join (manylinux_suffixes )} )_({ "" .join (valid_platform_tags )} )(\.|))+.whl$'
276+ )
277+ combined_match_pattern = rf"({ manylinux_wheel_file_match } )|({ manylinux_wheel_abi3_file_match } )"
278+ manylinux_wheel_file_match_pattern = re .compile (combined_match_pattern )
279+ return manylinux_wheel_file_match_pattern
246280
247281
248282class Zappa :
@@ -263,7 +297,7 @@ class Zappa:
263297 apigateway_policy = None
264298 cloudwatch_log_levels = ["OFF" , "ERROR" , "INFO" ]
265299 xray_tracing = False
266-
300+ architecture = None
267301 ##
268302 # Credentials
269303 ##
@@ -283,6 +317,7 @@ def __init__(
283317 tags = (),
284318 endpoint_urls = {},
285319 xray_tracing = False ,
320+ architecture = None ,
286321 ):
287322 """
288323 Instantiate this new Zappa instance, loading any custom credentials if necessary.
@@ -304,14 +339,14 @@ def __init__(
304339
305340 self .runtime = runtime
306341
307- # TODO: Support PEP600 properly (https://peps.python.org/pep-0600/)
308- self . manylinux_suffix_start = f"cp { self . runtime [ 6 :]. replace ( '.' , '' ) } "
309- self . manylinux_suffixes = ( "_2_24" , "2014" , "2010" , "1" )
310- # TODO: Support aarch64 architecture
311- self . manylinux_wheel_file_match = re . compile (
312- rf'^.* { self .manylinux_suffix_start } -(manylinux_\d+_\d+_x86_64[.])?manylinux( { "|" . join ( self . manylinux_suffixes ) } )_x86_64[.]whl$' # noqa: E501
313- )
314- self .manylinux_wheel_abi3_file_match = re . compile ( r"^.*cp3.-abi3-manylinux.*_x86_64[.]whl$" )
342+ if not architecture :
343+ architecture = X86_ARCHITECTURE
344+ if architecture not in VALID_ARCHITECTURES :
345+ raise ValueError ( f"Invalid architecture ' { architecture } '. Must be one of: { VALID_ARCHITECTURES } " )
346+
347+ self .architecture = architecture
348+
349+ self .manylinux_wheel_file_match = build_manylinux_wheel_file_match_pattern ( runtime , architecture )
315350
316351 self .endpoint_urls = endpoint_urls
317352 self .xray_tracing = xray_tracing
@@ -754,7 +789,7 @@ def splitpath(path):
754789 # use the compiled bytecode anyway..
755790 if filename [- 3 :] == ".py" and root [- 10 :] != "migrations" :
756791 abs_filename = os .path .join (root , filename )
757- abs_pyc_filename = abs_filename + "c"
792+ abs_pyc_filename = f" { abs_filename } c" # XXX.pyc
758793 if os .path .isfile (abs_pyc_filename ):
759794 # but only if the pyc is older than the py,
760795 # otherwise we'll deploy outdated code!
@@ -870,34 +905,33 @@ def get_cached_manylinux_wheel(self, package_name, package_version, disable_prog
870905 """
871906 Gets the locally stored version of a manylinux wheel. If one does not exist, the function downloads it.
872907 """
873- cached_wheels_dir = os . path . join (tempfile .gettempdir (), "cached_wheels" )
908+ cached_wheels_dir = Path (tempfile .gettempdir ()) / "cached_wheels"
874909
875- if not os . path . isdir ( cached_wheels_dir ):
876- os . makedirs ( cached_wheels_dir )
910+ if not cached_wheels_dir . is_dir () or not cached_wheels_dir . exists ( ):
911+ cached_wheels_dir . mkdir ( parents = True , exist_ok = True )
877912 else :
878913 # Check if we already have a cached copy
914+ # - get package name from prefix of the wheel file
879915 wheel_name = re .sub (r"[^\w\d.]+" , "_" , package_name , flags = re .UNICODE )
880- wheel_file = f"{ wheel_name } -{ package_version } -*_x86_64.whl"
881- wheel_path = os .path .join (cached_wheels_dir , wheel_file )
882-
883- for pathname in glob .iglob (wheel_path ):
884- if re .match (self .manylinux_wheel_file_match , pathname ):
885- logger .info (f" - { package_name } =={ package_version } : Using locally cached manylinux wheel" )
886- return pathname
887- elif re .match (self .manylinux_wheel_abi3_file_match , pathname ):
888- for manylinux_suffix in self .manylinux_suffixes :
889- if f"manylinux{ manylinux_suffix } _x86_64" in pathname :
890- logger .info (f" - { package_name } =={ package_version } : Using locally cached manylinux wheel" )
891- return pathname
916+ valid_architectures = (X86_ARCHITECTURE ,)
917+ if self .architecture == ARM_ARCHITECTURE :
918+ valid_architectures = (ARM_ARCHITECTURE , "aarch64" )
919+ for arch in valid_architectures :
920+ wheel_file_pattern = f"{ wheel_name } -{ package_version } -*_{ arch } .whl"
921+ for pathname in cached_wheels_dir .glob (wheel_file_pattern ):
922+ if self .manylinux_wheel_file_match .match (str (pathname )):
923+ logger .info (f" - { package_name } =={ package_version } : Using locally cached manylinux wheel" )
924+ return pathname
892925
893926 # The file is not cached, download it.
894927 wheel_url , filename = self .get_manylinux_wheel_url (package_name , package_version )
895928 if not wheel_url :
929+ logger .warning (f" - { package_name } =={ package_version } : No manylinux wheel found for this package" )
896930 return None
897931
898- wheel_path = os . path . join ( cached_wheels_dir , filename )
932+ wheel_path = cached_wheels_dir / filename
899933 logger .info (f" - { package_name } =={ package_version } : Downloading" )
900- with open (wheel_path , "wb" ) as f :
934+ with wheel_path . open ("wb" ) as f :
901935 self .download_url_with_progress (wheel_url , f , disable_progress )
902936
903937 if not zipfile .is_zipfile (wheel_path ):
@@ -917,23 +951,23 @@ def get_manylinux_wheel_url(self, package_name, package_version, ignore_cache: b
917951 every time.
918952 """
919953 cached_pypi_info_dir = Path (tempfile .gettempdir ()) / "cached_pypi_info"
920- if not cached_pypi_info_dir .is_dir ():
921- os . makedirs ( cached_pypi_info_dir )
954+ if not cached_pypi_info_dir .exists ():
955+ cached_pypi_info_dir . mkdir ( parents = True , exist_ok = True )
922956
923957 # Even though the metadata is for the package, we save it in a
924958 # filename that includes the package's version. This helps in
925959 # invalidating the cached file if the user moves to a different
926960 # version of the package.
927961 # Related: https://github.com/Miserlou/Zappa/issues/899
928962 data = None
929- json_file_name = "{0 !s}-{1 !s}.json". format ( package_name , package_version )
963+ json_file_name = f" { package_name !s} -{ package_version !s} .json"
930964 json_file_path = cached_pypi_info_dir / json_file_name
931965 if json_file_path .exists ():
932966 with json_file_path .open ("rb" ) as metafile :
933967 data = json .load (metafile )
934968
935969 if not data or ignore_cache :
936- url = "https://pypi.python.org/pypi/{}/json" . format ( package_name )
970+ url = f "https://pypi.python.org/pypi/{ package_name } /json"
937971 try :
938972 res = requests .get (url , timeout = float (os .environ .get ("PIP_TIMEOUT" , 1.5 )))
939973 data = res .json ()
@@ -949,14 +983,10 @@ def get_manylinux_wheel_url(self, package_name, package_version, ignore_cache: b
949983 return None , None
950984
951985 for f in data ["releases" ][package_version ]:
952- if re . match ( self .manylinux_wheel_file_match , f ["filename" ]):
986+ if self .manylinux_wheel_file_match . match ( f ["filename" ]):
953987 # Since we have already lowered package names in get_installed_packages
954988 # manylinux caching is not working for packages with capital case in names like MarkupSafe
955989 return f ["url" ], f ["filename" ].lower ()
956- elif re .match (self .manylinux_wheel_abi3_file_match , f ["filename" ]):
957- for manylinux_suffix in self .manylinux_suffixes :
958- if f"manylinux{ manylinux_suffix } _x86_64" in f ["filename" ]:
959- return f ["url" ], f ["filename" ].lower ()
960990 return None , None
961991
962992 ##
@@ -1126,6 +1156,8 @@ def create_lambda_function(
11261156 TracingConfig = {"Mode" : "Active" if self .xray_tracing else "PassThrough" },
11271157 SnapStart = {"ApplyOn" : snap_start if snap_start else "None" },
11281158 Layers = layers ,
1159+ # zappa currently only supports a single architecture, and uses a str value internally
1160+ Architectures = [self .architecture ],
11291161 )
11301162 if not docker_image_uri :
11311163 kwargs ["Runtime" ] = runtime
0 commit comments