1- from os .path import basename , dirname , exists , isdir , isfile , join , realpath , split
1+ from os .path import basename , dirname , exists , isdir , isfile , join , realpath , split , sep
22import glob
33
44import hashlib
77import sh
88import shutil
99import fnmatch
10+ import zipfile
1011import urllib .request
1112from urllib .request import urlretrieve
1213from os import listdir , unlink , environ , curdir , walk
2021import packaging .version
2122
2223from pythonforandroid .logger import (
23- logger , info , warning , debug , shprint , info_main )
24+ logger , info , warning , debug , shprint , info_main , error )
2425from pythonforandroid .util import (
2526 current_directory , ensure_dir , BuildInterruptingException , rmdir , move ,
2627 touch )
@@ -175,6 +176,7 @@ def download_file(self, url, target, cwd=None):
175176 """
176177 if not url :
177178 return
179+
178180 info ('Downloading {} from {}' .format (self .name , url ))
179181
180182 if cwd :
@@ -458,7 +460,6 @@ def unpack(self, arch):
458460 # apparently happens sometimes with
459461 # github zips
460462 pass
461- import zipfile
462463 fileh = zipfile .ZipFile (extraction_filename , 'r' )
463464 root_directory = fileh .filelist [0 ].filename .split ('/' )[0 ]
464465 if root_directory != basename (directory_name ):
@@ -837,6 +838,9 @@ class PythonRecipe(Recipe):
837838 on python2 or python3 which can break the dependency graph
838839 '''
839840
841+ hostpython_prerequisites = []
842+ '''List of hostpython packages required to build a recipe'''
843+
840844 def __init__ (self , * args , ** kwargs ):
841845 super ().__init__ (* args , ** kwargs )
842846
@@ -930,6 +934,7 @@ def should_build(self, arch):
930934 def build_arch (self , arch ):
931935 '''Install the Python module by calling setup.py install with
932936 the target Python dir.'''
937+ self .install_hostpython_prerequisites ()
933938 super ().build_arch (arch )
934939 self .install_python_package (arch )
935940
@@ -958,9 +963,13 @@ def install_python_package(self, arch, name=None, env=None, is_dir=True):
958963
959964 def get_hostrecipe_env (self , arch ):
960965 env = environ .copy ()
961- env ['PYTHONPATH' ] = join ( dirname ( self .real_hostpython_location ), 'Lib' , 'site-packages' )
966+ env ['PYTHONPATH' ] = self .hostpython_site_dir
962967 return env
963968
969+ @property
970+ def hostpython_site_dir (self ):
971+ return join (dirname (self .real_hostpython_location ), 'Lib' , 'site-packages' )
972+
964973 def install_hostpython_package (self , arch ):
965974 env = self .get_hostrecipe_env (arch )
966975 real_hostpython = sh .Command (self .real_hostpython_location )
@@ -969,6 +978,27 @@ def install_hostpython_package(self, arch):
969978 '--install-lib=Lib/site-packages' ,
970979 _env = env , * self .setup_extra_args )
971980
981+ @property
982+ def python_version (self ):
983+ return Recipe .get_recipe ("python3" , self .ctx ).version
984+
985+ def install_hostpython_prerequisites (self , force_upgrade = True ):
986+ if len (self .hostpython_prerequisites ) == 0 :
987+ return
988+ pip_options = [
989+ "install" ,
990+ * self .hostpython_prerequisites ,
991+ "--target" , self .hostpython_site_dir , "--python-version" ,
992+ self .python_version ,
993+ # Don't use sources, instead wheels
994+ "--only-binary=:all:" ,
995+ "--no-deps"
996+ ]
997+ if force_upgrade :
998+ pip_options .append ("--upgrade" )
999+ # Use system's pip
1000+ shprint (sh .pip , * pip_options )
1001+
9721002
9731003class CompiledComponentsPythonRecipe (PythonRecipe ):
9741004 pre_build_ext = False
@@ -1127,6 +1157,144 @@ def get_recipe_env(self, arch, with_flags_in_cc=True):
11271157 return env
11281158
11291159
1160+ class RustCompiledComponentsRecipe (PythonRecipe ):
1161+ # Rust toolchain codes
1162+ # https://doc.rust-lang.org/nightly/rustc/platform-support.html
1163+ RUST_ARCH_CODES = {
1164+ "arm64-v8a" : "aarch64-linux-android" ,
1165+ "armeabi-v7a" : "armv7-linux-androideabi" ,
1166+ "x86_64" : "x86_64-linux-android" ,
1167+ "x86" : "i686-linux-android" ,
1168+ }
1169+
1170+ # Build python wheel using `maturin` instead
1171+ # of default `python -m build [...]`
1172+ use_maturin = False
1173+
1174+ # Directory where to find built wheel
1175+ # For normal build: "dist/*.whl"
1176+ # For maturin: "target/wheels/*-linux_*.whl"
1177+ built_wheel_pattern = None
1178+
1179+ call_hostpython_via_targetpython = False
1180+
1181+ def __init__ (self , * arg , ** kwargs ):
1182+ super ().__init__ (* arg , ** kwargs )
1183+ self .append_deps_if_absent (["python3" ])
1184+ self .set_default_hostpython_deps ()
1185+ if not self .built_wheel_pattern :
1186+ self .built_wheel_pattern = (
1187+ "target/wheels/*-linux_*.whl"
1188+ if self .use_maturin
1189+ else "dist/*.whl"
1190+ )
1191+
1192+ def set_default_hostpython_deps (self ):
1193+ if not self .use_maturin :
1194+ self .hostpython_prerequisites += ["build" , "setuptools_rust" , "wheel" , "pyproject_hooks" ]
1195+ else :
1196+ self .hostpython_prerequisites += ["maturin" ]
1197+
1198+ def append_deps_if_absent (self , deps ):
1199+ for dep in deps :
1200+ if dep not in self .depends :
1201+ self .depends .append (dep )
1202+
1203+ def get_recipe_env (self , arch ):
1204+ env = super ().get_recipe_env (arch )
1205+
1206+ # Set rust build target
1207+ build_target = self .RUST_ARCH_CODES [arch .arch ]
1208+ cargo_linker_name = "CARGO_TARGET_{}_LINKER" .format (
1209+ build_target .upper ().replace ("-" , "_" )
1210+ )
1211+ env ["CARGO_BUILD_TARGET" ] = build_target
1212+ env [cargo_linker_name ] = join (
1213+ self .ctx .ndk .llvm_prebuilt_dir ,
1214+ "bin" ,
1215+ "{}{}-clang" .format (
1216+ # NDK's Clang format
1217+ build_target .replace ("7" , "7a" )
1218+ if build_target .startswith ("armv7" )
1219+ else build_target ,
1220+ self .ctx .ndk_api ,
1221+ ),
1222+ )
1223+ realpython_dir = Recipe .get_recipe ("python3" , self .ctx ).get_build_dir (arch .arch )
1224+
1225+ env ["RUSTFLAGS" ] = "-Clink-args=-L{} -L{}" .format (
1226+ self .ctx .get_libs_dir (arch .arch ), join (realpython_dir , "android-build" )
1227+ )
1228+
1229+ env ["PYO3_CROSS_LIB_DIR" ] = realpath (glob .glob (join (
1230+ realpython_dir , "android-build" , "build" ,
1231+ "lib.linux-*-{}/" .format (self .get_python_formatted_version ()),
1232+ ))[0 ])
1233+
1234+ info_main ("Ensuring rust build toolchain" )
1235+ shprint (sh .rustup , "target" , "add" , build_target )
1236+
1237+ # Add host python to PATH
1238+ env ["PATH" ] = ("{hostpython_dir}:{old_path}" ).format (
1239+ hostpython_dir = Recipe .get_recipe (
1240+ "hostpython3" , self .ctx
1241+ ).get_path_to_python (),
1242+ old_path = env ["PATH" ],
1243+ )
1244+ return env
1245+
1246+ def get_python_formatted_version (self ):
1247+ parsed_version = packaging .version .parse (self .python_version )
1248+ return f"{ parsed_version .major } .{ parsed_version .minor } "
1249+
1250+ def check_host_deps (self ):
1251+ if not hasattr (sh , "rustup" ):
1252+ error (
1253+ "`rustup` was not found on host system."
1254+ "Please install it using :"
1255+ "\n `curl https://sh.rustup.rs -sSf | sh`\n "
1256+ )
1257+ exit (1 )
1258+
1259+ def build_arch (self , arch ):
1260+ self .check_host_deps ()
1261+ self .install_hostpython_prerequisites ()
1262+ build_dir = self .get_build_dir (arch .arch )
1263+ env = self .get_recipe_env (arch )
1264+ built_wheel = None
1265+
1266+ # Copy the exec with version info
1267+ hostpython_exec = join (
1268+ sep ,
1269+ * self .hostpython_location .split (sep )[:- 1 ],
1270+ "python{}" .format (self .get_python_formatted_version ()),
1271+ )
1272+ shprint (sh .cp , self .hostpython_location , hostpython_exec )
1273+
1274+ with current_directory (build_dir ):
1275+ if self .use_maturin :
1276+ shprint (
1277+ sh .Command (join (self .hostpython_site_dir , "bin" , "maturin" )),
1278+ "build" , "--interpreter" , hostpython_exec , "--skip-auditwheel" ,
1279+ _env = env ,
1280+ )
1281+ else :
1282+ shprint (
1283+ sh .Command (hostpython_exec ),
1284+ "-m" , "build" , "--no-isolation" , "--skip-dependency-check" , "--wheel" ,
1285+ _env = env ,
1286+ )
1287+ # Find the built wheel
1288+ built_wheel = realpath (glob .glob (self .built_wheel_pattern )[0 ])
1289+
1290+ info ("Unzipping built wheel '{}'" .format (basename (built_wheel )))
1291+
1292+ # Unzip .whl file into site-packages
1293+ with zipfile .ZipFile (built_wheel , "r" ) as zip_ref :
1294+ zip_ref .extractall (self .ctx .get_python_install_dir (arch .arch ))
1295+ info ("Successfully installed '{}'" .format (basename (built_wheel )))
1296+
1297+
11301298class TargetPythonRecipe (Recipe ):
11311299 '''Class for target python recipes. Sets ctx.python_recipe to point to
11321300 itself, so as to know later what kind of Python was built or used.'''
0 commit comments