55from re import match
66
77import sh
8+ import subprocess
89import shutil
910import fnmatch
1011import zipfile
1112import urllib .request
1213from urllib .request import urlretrieve
13- from os import listdir , unlink , environ , curdir , walk
14+ from os import listdir , unlink , environ , curdir , walk , chmod
1415from sys import stdout
16+ from packaging .version import Version
1517from multiprocessing import cpu_count
1618import time
1719try :
@@ -966,7 +968,8 @@ def patch_shebangs(self, path, original_bin):
966968 # set correct shebang
967969 for file in listdir (path ):
968970 _file = join (path , file )
969- self .patch_shebang (_file , original_bin )
971+ if isfile (_file ):
972+ self .patch_shebang (_file , original_bin )
970973
971974 def get_recipe_env (self , arch = None , with_flags_in_cc = True ):
972975 if self ._host_recipe is None :
@@ -976,11 +979,7 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True):
976979 # Set the LANG, this isn't usually important but is a better default
977980 # as it occasionally matters how Python e.g. reads files
978981 env ['LANG' ] = "en_GB.UTF-8"
979-
980- # Binaries made by packages installed by pip
981- self .patch_shebangs (self ._host_recipe .local_bin , self ._host_recipe .python_exe )
982982 env ["PATH" ] = self ._host_recipe .local_bin + ":" + self ._host_recipe .site_bin + ":" + env ["PATH" ]
983-
984983 host_env = self .get_hostrecipe_env (arch )
985984 env ['PYTHONPATH' ] = host_env ["PYTHONPATH" ]
986985
@@ -1031,10 +1030,8 @@ def install_python_package(self, arch, name=None, env=None, is_dir=True):
10311030
10321031 def get_hostrecipe_env (self , arch = None ):
10331032 env = environ .copy ()
1034- _python_path = self ._host_recipe .get_path_to_python ()
1035- libdir = glob .glob (join (_python_path , "build" , "lib*" ))
1036- env ['PYTHONPATH' ] = self ._host_recipe .site_dir + ":" + join (
1037- _python_path , "Modules" ) + ":" + (libdir [0 ] if libdir else "" )
1033+ env ['PYTHONPATH' ] = ''
1034+ env ['HOME' ] = '/tmp'
10381035 return env
10391036
10401037 @property
@@ -1063,14 +1060,10 @@ def install_hostpython_prerequisites(self, packages=None, force_upgrade=True):
10631060 pip_options = [
10641061 "install" ,
10651062 * packages ,
1066- "--target" , self ._host_recipe .site_dir , "--python-version" ,
1067- self .ctx .python_recipe .version ,
1068- # Don't use sources, instead wheels
1069- "--only-binary=:all:" ,
1063+ "-q" ,
10701064 ]
10711065 if force_upgrade :
10721066 pip_options .append ("--upgrade" )
1073- # Use system's pip
10741067 pip_env = self .get_hostrecipe_env ()
10751068 shprint (self ._host_recipe .pip , * pip_options , _env = pip_env )
10761069
@@ -1284,7 +1277,7 @@ def lookup_prebuilt(self, arch):
12841277 pip_options .extend (["--dry-run" , "-q" ])
12851278 pip_env = self .get_hostrecipe_env ()
12861279 try :
1287- shprint (self ._host_recipe .pip , * pip_options , _env = pip_env )
1280+ shprint (self ._host_recipe .pip , * pip_options , _env = pip_env , silent = True )
12881281 except Exception :
12891282 return False
12901283 return True
@@ -1301,9 +1294,6 @@ def check_prebuilt(self, arch, msg=""):
13011294 return False
13021295
13031296 def get_recipe_env (self , arch , ** kwargs ):
1304- # Custom hostpython
1305- self .ctx .python_recipe .python_exe = join (
1306- self .ctx .python_recipe .get_build_dir (arch ), "android-build" , "python3" )
13071297 env = super ().get_recipe_env (arch , ** kwargs )
13081298 build_dir = self .get_build_dir (arch )
13091299 ensure_dir (build_dir )
@@ -1316,6 +1306,8 @@ def get_recipe_env(self, arch, **kwargs):
13161306 file .close ()
13171307
13181308 env ["DIST_EXTRA_CONFIG" ] = build_opts
1309+ python_recipe = Recipe .get_recipe ("python3" , self .ctx )
1310+ env ["INCLUDEPY" ] = python_recipe .include_root (arch .arch )
13191311 return env
13201312
13211313 @staticmethod
@@ -1387,18 +1379,14 @@ def build_arch(self, arch):
13871379 if not (isfile (join (build_dir , "pyproject.toml" )) or isfile (join (build_dir , "setup.py" ))):
13881380 warning ("Skipping build because it does not appear to be a Python project." )
13891381 return
1390-
13911382 self .install_hostpython_prerequisites (
13921383 packages = ["build[virtualenv]" , "pip" , "setuptools" , "patchelf" ] + self .hostpython_prerequisites
13931384 )
1394- self .patch_shebangs (self ._host_recipe .site_bin , self .real_hostpython_location )
13951385
13961386 env = self .get_recipe_env (arch , with_flags_in_cc = True )
13971387 # make build dir separately
13981388 sub_build_dir = join (build_dir , "p4a_android_build" )
13991389 ensure_dir (sub_build_dir )
1400- # copy hostpython to built python to ensure correct selection of libs and includes
1401- shprint (sh .cp , self .real_hostpython_location , self .ctx .python_recipe .python_exe )
14021390
14031391 build_args = [
14041392 "-m" ,
@@ -1411,7 +1399,7 @@ def build_arch(self, arch):
14111399 built_wheels = []
14121400 with current_directory (build_dir ):
14131401 shprint (
1414- sh .Command (self .ctx . python_recipe . python_exe ), * build_args , _env = env
1402+ sh .Command (self .real_hostpython_location ), * build_args , _env = env
14151403 )
14161404 built_wheels = [realpath (whl ) for whl in glob .glob ("dist/*.whl" )]
14171405 self .install_wheel (arch , built_wheels )
@@ -1421,7 +1409,7 @@ class MesonRecipe(PyProjectRecipe):
14211409 '''Recipe for projects which uses meson as build system'''
14221410
14231411 meson_version = "1.4.0"
1424- ninja_version = "1.11.1.1 "
1412+ pybind_version = "3.3.0 "
14251413
14261414 skip_python = False
14271415 '''If true, skips all Python build and installation steps.
@@ -1430,10 +1418,98 @@ class MesonRecipe(PyProjectRecipe):
14301418 def sanitize_flags (self , * flag_strings ):
14311419 return " " .join (flag_strings ).strip ().split (" " )
14321420
1421+ def get_wrapper_dir (self , arch ):
1422+ return join (self .get_build_dir (arch .arch ), "p4a_wrappers" )
1423+
1424+ def write_wrapper (self , arch , name , content ):
1425+ wrapper_dir = self .get_wrapper_dir (arch )
1426+ ensure_dir (wrapper_dir )
1427+ wrapper_path = join (wrapper_dir , name )
1428+ with open (wrapper_path , "w" ) as f :
1429+ f .write (content )
1430+ chmod (wrapper_path , 0o755 )
1431+ return wrapper_path
1432+
1433+ def get_python_wrapper (self , arch ):
1434+ """
1435+ Meson Python introspection runs on the host interpreter, but the
1436+ target Python (Android) cannot be executed on the build machine.
1437+
1438+ We therefore run host Python and override sysconfig data to emulate
1439+ the target Android Python environment during Meson introspection.
1440+ """
1441+ python_recipe = Recipe .get_recipe ('python3' , self .ctx )
1442+ target_prefix = python_recipe .get_python_root (arch )
1443+ python_file = join (self .ctx .root_dir , 'meson_python.py' )
1444+ _arch = {
1445+ "arm64-v8a" : ["aarch64" ],
1446+ "x86_64" : ["x86_64" ],
1447+ "armeabi-v7a" : ["arm" ],
1448+ "x86" : ["i686" ],
1449+ }[arch .arch ][0 ]
1450+
1451+ # Real values pulled from android
1452+ # PYTHON_MAJOR_VERSION -> 3
1453+ # PYTHON_MINOR_VERSION -> 14
1454+ # PLATFORM_TAG eg -> 'android-24-arm64_v8a'
1455+ # PYTHON_SUFFIX eg -> '.cpython-314-aarch64-linux-android.so'
1456+
1457+ _p_version = Version (python_recipe .version )
1458+ file_data = f"#!{ self .real_hostpython_location } "
1459+ file_data += f"\n TARGET_PYTHON_PREFIX='{ target_prefix } '"
1460+ file_data += f"\n PYTHON_MAJOR_VERSION='{ _p_version .major } '"
1461+ file_data += f"\n PYTHON_MINOR_VERSION='{ _p_version .minor } '"
1462+ file_data += f"\n PLATFORM_TAG='{ self .get_wheel_platform_tags (arch .arch , self .ctx )[0 ]} '"
1463+ file_data += f"\n PYTHON_SUFFIX='.cpython-{ _p_version .major } { _p_version .minor } -{ _arch } -linux-android.so'"
1464+
1465+ with open (python_file , "r" ) as f :
1466+ file_data += "\n " + f .read ()
1467+
1468+ return self .write_wrapper (arch , "python" , file_data )
1469+
1470+ def get_config_wrappers (self , arch , w_type : str ):
1471+ wrapper_name = ""
1472+ version = ""
1473+ include_path = ""
1474+
1475+ if w_type == "pybind11" :
1476+ wrapper_name = "pybind11-config"
1477+ include_path = join (self ._host_recipe .site_dir , "pybind11/include" )
1478+
1479+ version = None
1480+ try :
1481+ command = [self ._host_recipe .real_hostpython_location , "-c" , "import pybind11; print(pybind11.__version__)" ]
1482+ version = subprocess .check_output (command ).decode ('utf-8' ).strip ()
1483+ except Exception :
1484+ warning ("Unable to get pybind11 version" )
1485+ if version is None :
1486+ version = self .pybind_version
1487+
1488+ elif w_type == "numpy" :
1489+ wrapper_name = "numpy-config"
1490+ recipe = Recipe .get_recipe ("numpy" , self .ctx )
1491+ include_path = recipe .get_include (arch )
1492+ version = recipe .version
1493+ else :
1494+ raise ValueError (f"Unknown wrapper type: { w_type } " )
1495+
1496+ content = (
1497+ f"#!/bin/sh\n "
1498+ f"if [ \" $1\" = \" --version\" ]; then\n "
1499+ f" echo '{ version } '\n "
1500+ f"else\n "
1501+ f" echo '-I{ include_path } '\n "
1502+ f"fi\n "
1503+ )
1504+ return self .write_wrapper (arch , wrapper_name , content )
1505+
14331506 def get_recipe_meson_options (self , arch ):
14341507 env = self .get_recipe_env (arch , with_flags_in_cc = True )
14351508 return {
14361509 "binaries" : {
1510+ "pybind11-config" : self .get_config_wrappers (arch , "pybind11" ),
1511+ "numpy-config" : self .get_config_wrappers (arch , "numpy" ),
1512+ "python" : self .get_python_wrapper (arch ),
14371513 "c" : arch .get_clang_exe (with_target = True ),
14381514 "cpp" : arch .get_clang_exe (with_target = True , plus_plus = True ),
14391515 "ar" : self .ctx .ndk .llvm_ar ,
@@ -1504,13 +1580,18 @@ def build_arch(self, arch):
15041580 self .ensure_args ('-Csetup-args=--cross-file' , '-Csetup-args={}' .format (cross_file ))
15051581 # ensure ninja and meson
15061582 for dep in [
1507- "ninja=={}" . format ( self . ninja_version ) ,
1583+ "ninja" ,
15081584 "meson=={}" .format (self .meson_version ),
15091585 ]:
15101586 if dep not in self .hostpython_prerequisites :
15111587 self .hostpython_prerequisites .append (dep )
1588+
15121589 if not self .skip_python :
15131590 super ().build_arch (arch )
1591+ else :
1592+ self .install_hostpython_prerequisites (
1593+ packages = ["build[virtualenv]" , "pip" , "setuptools" , "patchelf" ] + self .hostpython_prerequisites
1594+ )
15141595
15151596
15161597class RustCompiledComponentsRecipe (PyProjectRecipe ):
@@ -1523,8 +1604,6 @@ class RustCompiledComponentsRecipe(PyProjectRecipe):
15231604 "x86" : "i686-linux-android" ,
15241605 }
15251606
1526- call_hostpython_via_targetpython = False
1527-
15281607 def get_recipe_env (self , arch , ** kwargs ):
15291608 env = super ().get_recipe_env (arch , ** kwargs )
15301609
@@ -1563,7 +1642,7 @@ def get_recipe_env(self, arch, **kwargs):
15631642 env ["PATH" ] = ("{hostpython_dir}:{old_path}" ).format (
15641643 hostpython_dir = Recipe .get_recipe (
15651644 "hostpython3" , self .ctx
1566- ).get_path_to_python () ,
1645+ ).local_bin ,
15671646 old_path = env ["PATH" ],
15681647 )
15691648 return env
0 commit comments