Skip to content

Commit 8750644

Browse files
committed
Add support for prebuilt wheels
1 parent 1fc0269 commit 8750644

6 files changed

Lines changed: 227 additions & 47 deletions

File tree

pythonforandroid/build.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from pythonforandroid.archs import ArchARM, ArchARMv7_a, ArchAarch_64, Archx86, Archx86_64
2222
from pythonforandroid.logger import (info, warning, info_notify, info_main, shprint, Out_Style, Out_Fore)
2323
from pythonforandroid.pythonpackage import get_package_name
24-
from pythonforandroid.recipe import CythonRecipe, Recipe
24+
from pythonforandroid.recipe import CythonRecipe, Recipe, PyProjectRecipe
2525
from pythonforandroid.recommendations import (
2626
check_ndk_version, check_target_api, check_ndk_api,
2727
RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API)
@@ -101,6 +101,12 @@ class Context:
101101

102102
java_build_tool = 'auto'
103103

104+
skip_prebuilt = False
105+
106+
extra_index_urls = []
107+
108+
use_prebuilt_version_for = []
109+
104110
@property
105111
def packages_path(self):
106112
'''Where packages are downloaded before being unpacked'''
@@ -667,7 +673,17 @@ def is_wheel_platform_independent(whl_name):
667673
return all(tag.platform == "any" for tag in tags)
668674

669675

670-
def process_python_modules(ctx, modules):
676+
def is_wheel_compatible(whl_name, arch, ctx):
677+
name, version, build, tags = parse_wheel_filename(whl_name)
678+
supported_tags = PyProjectRecipe.get_wheel_platform_tag(None, arch.arch, ctx=ctx)
679+
supported_tags.append("any")
680+
result = all(tag.platform in supported_tags for tag in tags)
681+
if not result:
682+
warning(f"Incompatible module : {whl_name}")
683+
return result
684+
685+
686+
def process_python_modules(ctx, modules, arch):
671687
"""Use pip --dry-run to resolve dependencies and filter for pure-Python packages
672688
"""
673689
modules = list(modules)
@@ -702,6 +718,7 @@ def process_python_modules(ctx, modules):
702718

703719
# setup hostpython recipe
704720
env = environ.copy()
721+
host_recipe = None
705722
try:
706723
host_recipe = Recipe.get_recipe("hostpython3", ctx)
707724
_python_path = host_recipe.get_path_to_python()
@@ -713,11 +730,28 @@ def process_python_modules(ctx, modules):
713730
# hostpython3 non available so we use system pip (like in tests)
714731
pip = sh.Command("pip")
715732

733+
# add platform tags
734+
platforms = []
735+
tags = PyProjectRecipe.get_wheel_platform_tag(None, arch.arch, ctx=ctx)
736+
for tag in tags:
737+
platforms.append(f"--platform={tag}")
738+
739+
if host_recipe is not None:
740+
platforms.extend(["--python-version", host_recipe.version])
741+
else:
742+
# tests?
743+
platforms.extend(["--python-version", "3.13.4"])
744+
745+
indices = []
746+
# add extra index urls
747+
for index in ctx.extra_index_urls:
748+
indices.extend(["--extra-index-url", index])
716749
try:
717750
shprint(
718751
pip, 'install', *modules,
719752
'--dry-run', '--break-system-packages', '--ignore-installed',
720-
'--report', path, '-q', _env=env
753+
'--disable-pip-version-check', '--only-binary=:all:',
754+
'--report', path, '-q', *platforms, *indices, _env=env
721755
)
722756
except Exception as e:
723757
warning(f"Auto module resolution failed: {e}")
@@ -751,7 +785,9 @@ def process_python_modules(ctx, modules):
751785
filename = basename(module["download_info"]["url"])
752786
pure_python = True
753787

754-
if (filename.endswith(".whl") and not is_wheel_platform_independent(filename)):
788+
if (
789+
filename.endswith(".whl") and not is_wheel_compatible(filename, arch, ctx)
790+
):
755791
any_not_pure_python = True
756792
pure_python = False
757793

@@ -793,7 +829,7 @@ def run_pymodules_install(ctx, arch, modules, project_dir=None,
793829

794830
info('*** PYTHON PACKAGE / PROJECT INSTALL STAGE FOR ARCH: {} ***'.format(arch))
795831

796-
modules = process_python_modules(ctx, modules)
832+
modules = process_python_modules(ctx, modules, arch)
797833

798834
modules = [m for m in modules if ctx.not_has_package(m, arch)]
799835

pythonforandroid/recipe.py

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -923,8 +923,7 @@ def real_hostpython_location(self):
923923
if host_name == 'hostpython3':
924924
return self._host_recipe.python_exe
925925
else:
926-
python_recipe = self.ctx.python_recipe
927-
return 'python{}'.format(python_recipe.version)
926+
return 'python{}'.format(self.ctx.python_recipe.version)
928927

929928
@property
930929
def hostpython_location(self):
@@ -1248,6 +1247,55 @@ class PyProjectRecipe(PythonRecipe):
12481247
extra_build_args = []
12491248
call_hostpython_via_targetpython = False
12501249

1250+
def get_pip_name(self):
1251+
name_str = self.name
1252+
if self.name not in self.ctx.use_prebuilt_version_for:
1253+
name_str += f"=={self.version}"
1254+
return name_str
1255+
1256+
def get_pip_install_args(self, arch):
1257+
python_recipe = Recipe.get_recipe("python3", self.ctx)
1258+
opts = [
1259+
"install",
1260+
self.get_pip_name(),
1261+
"--disable-pip-version-check",
1262+
"--python-version",
1263+
python_recipe.version,
1264+
"--only-binary=:all:",
1265+
]
1266+
# add platform tags
1267+
tags = self.get_wheel_platform_tag(arch.arch)
1268+
for tag in tags:
1269+
opts.append(f"--platform={tag}")
1270+
1271+
# add extra index urls
1272+
for index in self.ctx.extra_index_urls:
1273+
opts.extend(["--extra-index-url", index])
1274+
1275+
return opts
1276+
1277+
def lookup_prebuilt(self, arch):
1278+
pip_options = self.get_pip_install_args(arch)
1279+
# do not install
1280+
pip_options.extend(["--dry-run"])
1281+
pip_env = self.get_hostrecipe_env()
1282+
try:
1283+
shprint(self._host_recipe.pip, *pip_options, _env=pip_env)
1284+
except Exception as e:
1285+
warning(f"Lookup fail result: {e}")
1286+
return False
1287+
return True
1288+
1289+
def check_prebuilt(self, arch, msg=""):
1290+
if self.ctx.skip_prebuilt:
1291+
return False
1292+
1293+
if self.lookup_prebuilt(arch):
1294+
if msg != "":
1295+
info(f"Prebuilt pip wheel found, {msg}")
1296+
return True
1297+
return
1298+
12511299
def get_recipe_env(self, arch, **kwargs):
12521300
# Custom hostpython
12531301
self.ctx.python_recipe.python_exe = join(
@@ -1259,24 +1307,40 @@ def get_recipe_env(self, arch, **kwargs):
12591307

12601308
with open(build_opts, "w") as file:
12611309
file.write("[bdist_wheel]\nplat_name={}".format(
1262-
self.get_wheel_platform_tag(arch)
1310+
self.get_wheel_platform_tag(arch.arch)[0]
12631311
))
12641312
file.close()
12651313

12661314
env["DIST_EXTRA_CONFIG"] = build_opts
12671315
return env
12681316

1269-
def get_wheel_platform_tag(self, arch):
1317+
def get_wheel_platform_tag(self, arch, ctx=None):
1318+
if ctx is None:
1319+
ctx = self.ctx
12701320
# https://peps.python.org/pep-0738/#packaging
12711321
# official python only supports 64 bit:
12721322
# android_21_arm64_v8a
12731323
# android_21_x86_64
1274-
return f"android_{self.ctx.ndk_api}_" + {
1275-
"arm64-v8a": "arm64_v8a",
1276-
"x86_64": "x86_64",
1277-
"armeabi-v7a": "arm",
1278-
"x86": "i686",
1279-
}[arch.arch]
1324+
_suffix = {
1325+
"arm64-v8a": ["arm64_v8a", "aarch64"],
1326+
"x86_64": ["x86_64"],
1327+
"armeabi-v7a": ["arm"],
1328+
"x86": ["i686"],
1329+
}[arch]
1330+
return [f"android_{ctx.ndk_api}_" + _ for _ in _suffix]
1331+
1332+
def install_prebuilt_wheel(self, arch):
1333+
info("Installing prebuilt built wheel")
1334+
destination = self.ctx.get_python_install_dir(arch.arch)
1335+
pip_options = self.get_pip_install_args(arch)
1336+
pip_options.extend(["--target", destination])
1337+
pip_options.append("--upgrade")
1338+
pip_env = self.get_hostrecipe_env()
1339+
try:
1340+
shprint(self._host_recipe.pip, *pip_options, _env=pip_env)
1341+
except Exception:
1342+
return False
1343+
return True
12801344

12811345
def install_wheel(self, arch, built_wheels):
12821346
with patch_wheel_setuptools_logging():
@@ -1287,7 +1351,7 @@ def install_wheel(self, arch, built_wheels):
12871351
# Fix wheel platform tag
12881352
wheel_tag = wheel_tags(
12891353
_wheel,
1290-
platform_tags=self.get_wheel_platform_tag(arch),
1354+
platform_tags=self.get_wheel_platform_tag(arch.arch)[0],
12911355
remove=True,
12921356
)
12931357
selected_wheel = join(built_wheel_dir, wheel_tag)
@@ -1305,6 +1369,11 @@ def install_wheel(self, arch, built_wheels):
13051369
wf.close()
13061370

13071371
def build_arch(self, arch):
1372+
if self.check_prebuilt(arch, "skipping build_arch") is not None:
1373+
result = self.install_prebuilt_wheel(arch)
1374+
if result:
1375+
return
1376+
warning("Failed to install prebuilt wheel, falling back to build_arch")
13081377

13091378
build_dir = self.get_build_dir(arch.arch)
13101379
if not (isfile(join(build_dir, "pyproject.toml")) or isfile(join(build_dir, "setup.py"))):

pythonforandroid/recipes/hostpython3/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from os.path import join
77

88
from packaging.version import Version
9-
from pythonforandroid.logger import shprint
9+
from pythonforandroid.logger import shprint, error
1010
from pythonforandroid.recipe import Recipe
1111
from pythonforandroid.util import (
1212
BuildInterruptingException,
@@ -48,6 +48,16 @@ class HostPython3Recipe(Recipe):
4848

4949
patches = ["fix_ensurepip.patch"]
5050

51+
# apply version guard
52+
def download(self):
53+
python_recipe = Recipe.get_recipe("python3", self.ctx)
54+
if python_recipe.version != self.version:
55+
error(
56+
f"python3 should have same version as hostpython3, {python_recipe.version} != {self.version}"
57+
)
58+
exit(1)
59+
super().download()
60+
5161
@property
5262
def _exe_name(self):
5363
'''

pythonforandroid/recipes/python3/__init__.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -55,36 +55,45 @@ class Python3Recipe(TargetPythonRecipe):
5555
'''
5656

5757
version = '3.14.2'
58-
_p_version = Version(version)
5958
url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz'
6059
name = 'python3'
6160

62-
patches = [
63-
'patches/pyconfig_detection.patch',
64-
'patches/reproducible-buildinfo.diff',
65-
]
61+
@property
62+
def _p_version(self):
63+
# as version is dynamic
64+
return Version(self.version)
6665

67-
if _p_version.major == 3 and _p_version.minor == 7:
68-
patches += [
69-
'patches/py3.7.1_fix-ctypes-util-find-library.patch',
70-
'patches/py3.7.1_fix-zlib-version.patch',
66+
@property
67+
def patches(self):
68+
patches = [
69+
'patches/pyconfig_detection.patch',
70+
'patches/reproducible-buildinfo.diff',
7171
]
72+
_p_version = self._p_version
7273

73-
if 8 <= _p_version.minor <= 10:
74-
patches.append('patches/py3.8.1.patch')
74+
if _p_version.major == 3 and _p_version.minor == 7:
75+
patches += [
76+
'patches/py3.7.1_fix-ctypes-util-find-library.patch',
77+
'patches/py3.7.1_fix-zlib-version.patch',
78+
]
7579

76-
if _p_version.minor >= 11:
77-
patches.append('patches/cpython-311-ctypes-find-library.patch')
80+
if 8 <= _p_version.minor <= 10:
81+
patches.append('patches/py3.8.1.patch')
7882

79-
if _p_version.minor >= 14:
80-
patches.append('patches/3.14_armv7l_fix.patch')
81-
patches.append('patches/3.14_fix_remote_debug.patch')
83+
if _p_version.minor >= 11:
84+
patches.append('patches/cpython-311-ctypes-find-library.patch')
8285

83-
if shutil.which('lld') is not None:
84-
if _p_version.minor == 7:
85-
patches.append("patches/py3.7.1_fix_cortex_a8.patch")
86-
elif _p_version.minor >= 8:
87-
patches.append("patches/py3.8.1_fix_cortex_a8.patch")
86+
if _p_version.minor >= 14:
87+
patches.append('patches/3.14_armv7l_fix.patch')
88+
patches.append('patches/3.14_fix_remote_debug.patch')
89+
90+
if shutil.which('lld') is not None:
91+
if _p_version.minor == 7:
92+
patches.append("patches/py3.7.1_fix_cortex_a8.patch")
93+
elif _p_version.minor >= 8:
94+
patches.append("patches/py3.8.1_fix_cortex_a8.patch")
95+
96+
return patches
8897

8998
depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi']
9099
# those optional depends allow us to build python compression modules:
@@ -116,11 +125,6 @@ class Python3Recipe(TargetPythonRecipe):
116125
'ac_cv_header_bzlib_h=no',
117126
]
118127

119-
if _p_version.minor >= 11:
120-
configure_args.extend([
121-
'--with-build-python={python_host_bin}',
122-
])
123-
124128
'''The configure arguments needed to build the python recipe. Those are
125129
used in method :meth:`build_arch` (if not overwritten like python3's
126130
recipe does).
@@ -317,6 +321,9 @@ def add_flags(include_flags, link_dirs, link_libs):
317321
env['ZLIB_VERSION'] = line.replace('#define ZLIB_VERSION ', '')
318322
add_flags(' -I' + zlib_includes, ' -L' + zlib_lib_path, ' -lz')
319323

324+
if self._p_version.minor >= 11:
325+
self.configure_args.append('--with-build-python={python_host_bin}')
326+
320327
if self._p_version.minor >= 13 and self.disable_gil:
321328
self.configure_args.append("--disable-gil")
322329

0 commit comments

Comments
 (0)