Skip to content

Commit 869c74b

Browse files
authored
fix PYTHONPATH hacks (#3301)
* fix PYTHONPATH hacks * fix android recipe * fix numpy and matplotlib build * no shebang patching required now * fix kivy master build * fix numpy build * more wrappers for meson recipe * flake8 fix * add back pandas host preq * fix bug in logger * add back cython for pandas
1 parent 2f107b1 commit 869c74b

15 files changed

Lines changed: 310 additions & 160 deletions

File tree

pythonforandroid/build.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -728,10 +728,6 @@ def process_python_modules(ctx, modules, arch):
728728
host_recipe = None
729729
try:
730730
host_recipe = Recipe.get_recipe("hostpython3", ctx)
731-
_python_path = host_recipe.get_path_to_python()
732-
libdir = glob.glob(join(_python_path, "build", "lib*"))
733-
env['PYTHONPATH'] = host_recipe.site_dir + ":" + join(
734-
_python_path, "Modules") + ":" + (libdir[0] if libdir else "")
735731
pip = host_recipe.pip
736732
except Exception:
737733
# hostpython3 is unavailable, so fall back to system pip

pythonforandroid/logger.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ def shprint(command, *args, **kwargs):
135135
kwargs["_out_bufsize"] = 1
136136
kwargs["_err_to_out"] = True
137137
kwargs["_bg"] = True
138+
139+
# silent mode
140+
silent = kwargs.get("silent", False)
141+
kwargs.pop("silent", False)
142+
if silent:
143+
kwargs["_out"] = None
144+
138145
is_critical = kwargs.pop('_critical', False)
139146
tail_n = kwargs.pop('_tail', None)
140147
full_debug = False
@@ -190,6 +197,11 @@ def shprint(command, *args, **kwargs):
190197
Err_Style.RESET_ALL, ' ', width=(columns - 1)))
191198
stdout.flush()
192199
except sh.ErrorReturnCode as err:
200+
if silent:
201+
if is_critical:
202+
exit(1)
203+
else:
204+
raise
193205
if need_closing_newline:
194206
stdout.write('{}\r{:>{width}}\r'.format(
195207
Err_Style.RESET_ALL, ' ', width=(columns - 1)))

pythonforandroid/meson_python.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import sys
2+
import json
3+
import subprocess
4+
import importlib.util
5+
from os.path import join
6+
from glob import glob
7+
8+
9+
class C:
10+
TARGET_PYTHON_PREFIX = globals().get("TARGET_PYTHON_PREFIX")
11+
PYTHON_MAJOR_VERSION = globals().get("PYTHON_MAJOR_VERSION", sys.version_info.major)
12+
PYTHON_MINOR_VERSION = globals().get("PYTHON_MINOR_VERSION", sys.version_info.minor)
13+
PLATFORM_TAG = globals().get("PLATFORM_TAG", sys.platform)
14+
PYTHON_SUFFIX = globals().get("PYTHON_SUFFIX", "")
15+
16+
17+
def handle_python_info():
18+
19+
prefix = C.TARGET_PYTHON_PREFIX
20+
21+
sysconfig_file = glob(join(prefix, "lib/python3.14/_sysconfigdata*.py"))[0]
22+
23+
spec = importlib.util.spec_from_file_location("_android_cfg", sysconfig_file)
24+
cfg = importlib.util.module_from_spec(spec)
25+
spec.loader.exec_module(cfg)
26+
27+
ver_python = f"python{C.PYTHON_MAJOR_VERSION}.{C.PYTHON_MINOR_VERSION}"
28+
29+
site_path = join(prefix, f"lib/{ver_python}/site-packages")
30+
31+
android_paths = {
32+
"stdlib": join(prefix, "lib"),
33+
"platstdlib": join(prefix, "lib"),
34+
"purelib": site_path,
35+
"platlib": site_path,
36+
"include": join(prefix, f"include/{ver_python}/"),
37+
"platinclude": join(prefix, f"include/{ver_python}/"),
38+
"scripts": join(prefix, "bin"),
39+
"data": prefix,
40+
}
41+
42+
print(
43+
json.dumps(
44+
{
45+
"variables": cfg.build_time_vars,
46+
"paths": android_paths,
47+
"sysconfig_paths": android_paths,
48+
"install_paths": android_paths,
49+
"version": f"{C.PYTHON_MAJOR_VERSION}.{C.PYTHON_MINOR_VERSION}",
50+
"platform": C.PLATFORM_TAG,
51+
"is_pypy": False,
52+
"is_venv": False,
53+
"link_libpython": True,
54+
"suffix": C.PYTHON_SUFFIX,
55+
"limited_api_suffix": ".abi3.so",
56+
"is_freethreaded": False,
57+
}
58+
)
59+
)
60+
61+
62+
WHITELIST = {
63+
"python_info.py": handle_python_info,
64+
}
65+
66+
67+
args = sys.argv[1:]
68+
69+
if args:
70+
cmd = args[0]
71+
name = cmd.split("/")[-1]
72+
73+
if name in WHITELIST:
74+
sys.exit(WHITELIST[name]())
75+
76+
sys.exit(subprocess.run([sys.executable] + args).returncode)

pythonforandroid/recipe.py

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
from re import match
66

77
import sh
8+
import subprocess
89
import shutil
910
import fnmatch
1011
import zipfile
1112
import urllib.request
1213
from urllib.request import urlretrieve
13-
from os import listdir, unlink, environ, curdir, walk
14+
from os import listdir, unlink, environ, curdir, walk, chmod
1415
from sys import stdout
16+
from packaging.version import Version
1517
from multiprocessing import cpu_count
1618
import time
1719
try:
@@ -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"\nTARGET_PYTHON_PREFIX='{target_prefix}'"
1460+
file_data += f"\nPYTHON_MAJOR_VERSION='{_p_version.major}'"
1461+
file_data += f"\nPYTHON_MINOR_VERSION='{_p_version.minor}'"
1462+
file_data += f"\nPLATFORM_TAG='{self.get_wheel_platform_tags(arch.arch, self.ctx)[0]}'"
1463+
file_data += f"\nPYTHON_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

15161597
class 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
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[build-system]
2+
requires = [
3+
"setuptools",
4+
"wheel",
5+
"Cython>=0.29,<3.1"
6+
]
7+
build-backend = "setuptools.build_meta"

0 commit comments

Comments
 (0)