Skip to content

Commit d066c73

Browse files
committed
[WIP] toolchain: auto resolve deps
1 parent 6494ac1 commit d066c73

File tree

5 files changed

+154
-14
lines changed

5 files changed

+154
-14
lines changed

pythonforandroid/build.py

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,24 @@
22
import copy
33
import glob
44
import os
5+
import json
6+
import tempfile
57
from os import environ
68
from os.path import (
7-
abspath, join, realpath, dirname, expanduser, exists
9+
abspath, join, realpath, dirname, expanduser, exists, basename
810
)
911
import re
1012
import shutil
1113
import subprocess
1214

1315
import sh
1416

17+
from packaging.utils import parse_wheel_filename
18+
from packaging.requirements import Requirement
19+
1520
from pythonforandroid.androidndk import AndroidNDK
1621
from pythonforandroid.archs import ArchARM, ArchARMv7_a, ArchAarch_64, Archx86, Archx86_64
17-
from pythonforandroid.logger import (info, warning, info_notify, info_main, shprint)
22+
from pythonforandroid.logger import (info, warning, info_notify, info_main, shprint, Out_Style, Out_Fore)
1823
from pythonforandroid.pythonpackage import get_package_name
1924
from pythonforandroid.recipe import CythonRecipe, Recipe
2025
from pythonforandroid.recommendations import (
@@ -90,6 +95,8 @@ class Context:
9095

9196
recipe_build_order = None # Will hold the list of all built recipes
9297

98+
python_modules = None # Will hold resolved pure python packages
99+
93100
symlink_bootstrap_files = False # If True, will symlink instead of copying during build
94101

95102
java_build_tool = 'auto'
@@ -444,6 +451,12 @@ def has_package(self, name, arch=None):
444451
# Failed to look up any meaningful name.
445452
return False
446453

454+
# normalize name to remove version tags
455+
try:
456+
name = Requirement(name).name
457+
except Exception:
458+
pass
459+
447460
# Try to look up recipe by name:
448461
try:
449462
recipe = Recipe.get_recipe(name, self)
@@ -649,6 +662,92 @@ def run_setuppy_install(ctx, project_dir, env=None, arch=None):
649662
os.remove("._tmp_p4a_recipe_constraints.txt")
650663

651664

665+
def is_wheel_platform_independent(whl_name):
666+
name, version, build, tags = parse_wheel_filename(whl_name)
667+
return all(tag.platform == "any" for tag in tags)
668+
669+
670+
def process_python_modules(ctx, modules):
671+
"""Use pip --dry-run to resolve dependencies and filter for pure-Python packages
672+
"""
673+
modules = list(modules)
674+
build_order = list(ctx.build_order)
675+
_requirement_names = []
676+
processed_modules = []
677+
678+
for module in modules+build_order:
679+
try:
680+
_requirement_names.append(Requirement(module).name)
681+
except Exception:
682+
processed_modules.append(module)
683+
if module in modules:
684+
modules.remove(module)
685+
686+
if len(processed_modules) > 0:
687+
warning(f'Ignored by module resolver : {processed_modules}')
688+
689+
processed_modules.extend(modules)
690+
691+
# temp file for pip report
692+
fd, path = tempfile.mkstemp()
693+
os.close(fd)
694+
695+
host_recipe = Recipe.get_recipe("hostpython3", ctx)
696+
697+
if not exists(path):
698+
shprint(
699+
host_recipe.pip, 'install', *modules,
700+
'--dry-run', '--break-system-packages', '--ignore-installed',
701+
'--report', path, '-q'
702+
)
703+
704+
with open(path, "r") as f:
705+
report = json.load(f)
706+
707+
os.remove(path)
708+
709+
info('Extra resolved pure python dependencies :')
710+
711+
ignored_str = " (ignored)"
712+
# did we find any non pure python package?
713+
any_not_pure_python = False
714+
715+
info(" ")
716+
for module in report["install"]:
717+
718+
mname = module["metadata"]["name"]
719+
mver = module["metadata"]["version"]
720+
filename = basename(module["download_info"]["url"])
721+
pure_python = True
722+
723+
if (filename.endswith(".whl") and not is_wheel_platform_independent(filename)):
724+
any_not_pure_python = True
725+
pure_python = False
726+
727+
# does this module matches any recipe name?
728+
if mname.lower() in _requirement_names:
729+
continue
730+
731+
color = Out_Fore.GREEN if pure_python else Out_Fore.RED
732+
ignored = "" if pure_python else ignored_str
733+
734+
info(
735+
f" {color}{mname}{Out_Fore.WHITE} : "
736+
f"{Out_Style.BRIGHT}{mver}{Out_Style.RESET_ALL}"
737+
f"{ignored}"
738+
)
739+
740+
if pure_python:
741+
processed_modules.append(f"{mname}=={mver}")
742+
info(" ")
743+
744+
if any_not_pure_python:
745+
warning("Some packages were ignored because they are not pure Python.")
746+
warning("To install the ignored packages, explicitly list them in your requirements file.")
747+
748+
return processed_modules
749+
750+
652751
def run_pymodules_install(ctx, arch, modules, project_dir=None,
653752
ignore_setup_py=False):
654753
""" This function will take care of all non-recipe things, by:
@@ -663,6 +762,7 @@ def run_pymodules_install(ctx, arch, modules, project_dir=None,
663762

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

765+
modules = process_python_modules(ctx, modules)
666766
modules = [m for m in modules if ctx.not_has_package(m, arch)]
667767

668768
# We change current working directory later, so this has to be an absolute

pythonforandroid/recipe.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,7 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True):
979979
env['LANG'] = "en_GB.UTF-8"
980980

981981
# Binaries made by packages installed by pip
982-
self.patch_shebangs(self._host_recipe.local_bin, self.real_hostpython_location)
982+
self.patch_shebangs(self._host_recipe.local_bin, self._host_recipe.python_exe)
983983
env["PATH"] = self._host_recipe.local_bin + ":" + self._host_recipe.site_bin + ":" + env["PATH"]
984984

985985
host_env = self.get_hostrecipe_env(arch)
@@ -1022,10 +1022,9 @@ def install_python_package(self, arch, name=None, env=None, is_dir=True):
10221022

10231023
info('Installing {} into site-packages'.format(self.name))
10241024

1025-
hostpython = sh.Command(self.hostpython_location)
10261025
hpenv = env.copy()
10271026
with current_directory(self.get_build_dir(arch.arch)):
1028-
shprint(hostpython, '-m', 'pip', 'install', '.',
1027+
shprint(self._host_recipe.pip, 'install', '.',
10291028
'--compile', '--target',
10301029
self.ctx.get_python_install_dir(arch.arch),
10311030
_env=hpenv, *self.setup_extra_args
@@ -1045,8 +1044,7 @@ def hostpython_site_dir(self):
10451044

10461045
def install_hostpython_package(self, arch):
10471046
env = self.get_hostrecipe_env(arch)
1048-
real_hostpython = sh.Command(self.real_hostpython_location)
1049-
shprint(real_hostpython, '-m', 'pip', 'install', '.',
1047+
shprint(self._host_recipe.pip, 'install', '.',
10501048
'--compile',
10511049
'--root={}'.format(self._host_recipe.site_root),
10521050
_env=env, *self.setup_extra_args)
@@ -1075,8 +1073,7 @@ def install_hostpython_prerequisites(self, packages=None, force_upgrade=True):
10751073
pip_options.append("--upgrade")
10761074
# Use system's pip
10771075
pip_env = self.get_hostrecipe_env()
1078-
pip_env["HOME"] = "/tmp"
1079-
shprint(sh.Command(self.real_hostpython_location), "-m", "pip", *pip_options, _env=pip_env)
1076+
shprint(self._host_recipe.pip, *pip_options, _env=pip_env)
10801077

10811078
def restore_hostpython_prerequisites(self, packages):
10821079
_packages = []
@@ -1270,10 +1267,14 @@ def get_recipe_env(self, arch, **kwargs):
12701267
return env
12711268

12721269
def get_wheel_platform_tag(self, arch):
1270+
# https://peps.python.org/pep-0738/#packaging
1271+
# official python only supports 64 bit:
1272+
# android_21_arm64_v8a
1273+
# android_21_x86_64
12731274
return f"android_{self.ctx.ndk_api}_" + {
1274-
"armeabi-v7a": "arm",
1275-
"arm64-v8a": "aarch64",
1275+
"arm64-v8a": "arm64_v8a",
12761276
"x86_64": "x86_64",
1277+
"armeabi-v7a": "arm",
12771278
"x86": "i686",
12781279
}[arch.arch]
12791280

pythonforandroid/recipes/hostpython3/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class HostPython3Recipe(Recipe):
3636
:class:`~pythonforandroid.python.HostPythonRecipe`
3737
'''
3838

39-
version = '3.14.0'
39+
version = '3.14.2'
4040

4141
url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz'
4242
'''The default url to download our host python recipe. This url will
@@ -113,6 +113,14 @@ def site_dir(self):
113113
f"usr/local/lib/python{p_version.major}.{p_version.minor}/site-packages/"
114114
)
115115

116+
@property
117+
def _pip(self):
118+
return join(self.local_bin, "pip3")
119+
120+
@property
121+
def pip(self):
122+
return sh.Command(self._pip)
123+
116124
def build_arch(self, arch):
117125
env = self.get_recipe_env(arch)
118126

@@ -160,10 +168,12 @@ def build_arch(self, arch):
160168

161169
ensure_dir(self.site_root)
162170
self.ctx.hostpython = self.python_exe
171+
163172
if build_configured:
173+
164174
shprint(
165175
sh.Command(self.python_exe), "-m", "ensurepip", "--root", self.site_root, "-U",
166-
_env={"HOME": "/tmp"}
176+
_env={"PATH": self.local_bin}
167177
)
168178

169179

pythonforandroid/recipes/python3/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Python3Recipe(TargetPythonRecipe):
5454
:class:`~pythonforandroid.python.GuestPythonRecipe`
5555
'''
5656

57-
version = '3.14.0'
57+
version = '3.14.2'
5858
_p_version = Version(version)
5959
url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz'
6060
name = 'python3'
@@ -78,6 +78,7 @@ class Python3Recipe(TargetPythonRecipe):
7878

7979
if _p_version.minor >= 14:
8080
patches.append('patches/3.14_armv7l_fix.patch')
81+
patches.append('patches/3.14_fix_remote_debug.patch')
8182

8283
if shutil.which('lld') is not None:
8384
if _p_version.minor == 7:
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
diff '--color=auto' -uNr cpython-3.14.2/Modules/_remote_debugging_module.c cpython-3.14.2.mod/Modules/_remote_debugging_module.c
2+
--- cpython-3.14.2/Modules/_remote_debugging_module.c 2025-12-05 22:19:16.000000000 +0530
3+
+++ cpython-3.14.2.mod/Modules/_remote_debugging_module.c 2025-12-13 20:22:44.011497868 +0530
4+
@@ -812,7 +812,9 @@
5+
PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process.");
6+
_PyErr_ChainExceptions1(exc);
7+
}
8+
-#elif defined(__linux__)
9+
+
10+
+// https://github.com/python/cpython/commit/1963e701001839389cfb1b11d803b0743f4705d7
11+
+#elif defined(__linux__) && HAVE_PROCESS_VM_READV
12+
// On Linux, search for asyncio debug in executable or DLL
13+
address = search_linux_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython");
14+
if (address == 0) {
15+
diff '--color=auto' -uNr cpython-3.14.2/Python/remote_debug.h cpython-3.14.2.mod/Python/remote_debug.h
16+
--- cpython-3.14.2/Python/remote_debug.h 2025-12-05 22:19:16.000000000 +0530
17+
+++ cpython-3.14.2.mod/Python/remote_debug.h 2025-12-13 20:23:27.917518543 +0530
18+
@@ -881,7 +881,9 @@
19+
handle->pid);
20+
_PyErr_ChainExceptions1(exc);
21+
}
22+
-#elif defined(__linux__)
23+
+
24+
+// https://github.com/python/cpython/commit/1963e701001839389cfb1b11d803b0743f4705d7
25+
+#elif defined(__linux__) && HAVE_PROCESS_VM_READV
26+
// On Linux, search for 'python' in executable or DLL
27+
address = search_linux_map_for_section(handle, "PyRuntime", "python");
28+
if (address == 0) {

0 commit comments

Comments
 (0)