Skip to content

Commit bd3c5b1

Browse files
committed
recipe: introduce PyProjectRecipe and MesonRecipe
1 parent 8110faf commit bd3c5b1

File tree

13 files changed

+228
-176
lines changed

13 files changed

+228
-176
lines changed

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ RUN ${RETRY} apt -y update -qq > /dev/null \
5757
ant \
5858
autoconf \
5959
automake \
60+
autopoint \
6061
ccache \
6162
cmake \
6263
g++ \

doc/source/quickstart.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ the following command (re-adapted from the `Dockerfile` we use to perform CI bui
7272
ant \
7373
autoconf \
7474
automake \
75+
autopoint \
7576
ccache \
7677
cmake \
7778
g++ \

pythonforandroid/recipe.py

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,8 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True):
899899
# Set the LANG, this isn't usually important but is a better default
900900
# as it occasionally matters how Python e.g. reads files
901901
env['LANG'] = "en_GB.UTF-8"
902+
# Binaries made by packages installed by pip
903+
env["PATH"] = join(self.hostpython_site_dir, "bin") + ":" + env["PATH"]
902904

903905
if not self.call_hostpython_via_targetpython:
904906
env['CFLAGS'] += ' -I{}'.format(
@@ -982,9 +984,17 @@ def install_hostpython_package(self, arch):
982984
def python_version(self):
983985
return Recipe.get_recipe("python3", self.ctx).version
984986

985-
def install_hostpython_prerequisites(self, force_upgrade=True):
986-
if len(self.hostpython_prerequisites) == 0:
987+
def get_python_formatted_version(self):
988+
parsed_version = packaging.version.parse(self.python_version)
989+
return f"{parsed_version.major}.{parsed_version.minor}"
990+
991+
def install_hostpython_prerequisites(self, packages=None, force_upgrade=True):
992+
if not packages:
993+
packages = self.hostpython_prerequisites
994+
995+
if len(packages) == 0:
987996
return
997+
988998
pip_options = [
989999
"install",
9901000
*self.hostpython_prerequisites,
@@ -999,6 +1009,10 @@ def install_hostpython_prerequisites(self, force_upgrade=True):
9991009
# Use system's pip
10001010
shprint(sh.pip, *pip_options)
10011011

1012+
def restore_hostpython_prerequisites(self, package):
1013+
original_version = Recipe.get_recipe(package, self.ctx).version
1014+
self.install_hostpython_prerequisites(packages=[package + "==" + original_version])
1015+
10021016

10031017
class CompiledComponentsPythonRecipe(PythonRecipe):
10041018
pre_build_ext = False
@@ -1243,10 +1257,6 @@ def get_recipe_env(self, arch):
12431257
)
12441258
return env
12451259

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-
12501260
def check_host_deps(self):
12511261
if not hasattr(sh, "rustup"):
12521262
error(
@@ -1295,6 +1305,145 @@ def build_arch(self, arch):
12951305
info("Successfully installed '{}'".format(basename(built_wheel)))
12961306

12971307

1308+
class PyProjectRecipe(PythonRecipe):
1309+
'''Recipe for projects which containes `pyproject.toml`'''
1310+
1311+
default_deps = ["build", "wheel", "pyproject_hooks",
1312+
"setuptools", "packaging", "pyproject_metadata"]
1313+
# Extra args to pass to `python -m build ...`
1314+
extra_build_args = []
1315+
hostpython_prerequisites = default_deps
1316+
call_hostpython_via_targetpython = False
1317+
1318+
def ensure_default_deps(self):
1319+
for dep in self.default_deps:
1320+
if dep not in self.hostpython_prerequisites:
1321+
self.hostpython_prerequisites.append(dep)
1322+
1323+
def build_arch(self, arch):
1324+
self.ensure_default_deps()
1325+
self.install_hostpython_prerequisites()
1326+
1327+
build_dir = self.get_build_dir(arch.arch)
1328+
env = self.get_recipe_env(arch, with_flags_in_cc=True)
1329+
built_wheel = None
1330+
info(str(env))
1331+
1332+
# make build dir separatly
1333+
sub_build_dir = join(build_dir, "p4a_android_build")
1334+
ensure_dir(sub_build_dir)
1335+
1336+
# Copying hostpython to the Python build ensures that it correctly identifies libraries and include files.
1337+
python_recipe = Recipe.get_recipe("python3", self.ctx)
1338+
custom_python = join(python_recipe.get_build_dir(arch), "android-build", "python3")
1339+
shprint(sh.cp, self.real_hostpython_location, custom_python)
1340+
1341+
build_args = [
1342+
"-m",
1343+
"build",
1344+
"--no-isolation",
1345+
"--skip-dependency-check",
1346+
"--wheel",
1347+
"--config-setting",
1348+
"builddir={}".format(sub_build_dir),
1349+
] + self.extra_build_args
1350+
1351+
with current_directory(build_dir):
1352+
shprint(
1353+
sh.Command(custom_python), *build_args, _env=env
1354+
)
1355+
built_wheel = realpath(glob.glob("dist/*.whl")[0])
1356+
1357+
info("Unzipping built wheel '{}'".format(basename(built_wheel)))
1358+
1359+
with zipfile.ZipFile(built_wheel, "r") as zip_ref:
1360+
zip_ref.extractall(self.ctx.get_python_install_dir(arch.arch))
1361+
info("Successfully installed '{}'".format(basename(built_wheel)))
1362+
1363+
1364+
class MesonRecipe(PyProjectRecipe):
1365+
'''Recipe for projects which uses meson as build system'''
1366+
1367+
def sanitize_flags(self, *flag_strings):
1368+
return " ".join(flag_strings).strip().split(" ")
1369+
1370+
def get_recipe_meson_options(self, arch):
1371+
"""Writes python dict to meson config file"""
1372+
env = self.get_recipe_env(arch, with_flags_in_cc=True)
1373+
return {
1374+
"binaries": {
1375+
"c": arch.get_clang_exe(with_target=True),
1376+
"cpp": arch.get_clang_exe(with_target=True, plus_plus=True),
1377+
"ar": self.ctx.ndk.llvm_ar,
1378+
"strip": self.ctx.ndk.llvm_strip,
1379+
},
1380+
"built-in options": {
1381+
"c_args": self.sanitize_flags(env["CFLAGS"], env["CPPFLAGS"]),
1382+
"cpp_args": self.sanitize_flags(env["CXXFLAGS"], env["CPPFLAGS"]),
1383+
"c_link_args": self.sanitize_flags(env["LDFLAGS"]),
1384+
"cpp_link_args": self.sanitize_flags(env["LDFLAGS"]),
1385+
},
1386+
"properties": {
1387+
"needs_exe_wrapper": True,
1388+
"sys_root": self.ctx.ndk.sysroot
1389+
},
1390+
"host_machine": {
1391+
"cpu_family": {
1392+
"arm64-v8a": "aarch64",
1393+
"armeabi-v7a": "arm",
1394+
"x86_64": "x86_64",
1395+
"x86": "x86"
1396+
}[arch.arch],
1397+
"cpu": {
1398+
"arm64-v8a": "aarch64",
1399+
"armeabi-v7a": "armv7",
1400+
"x86_64": "x86_64",
1401+
"x86": "i686"
1402+
}[arch.arch],
1403+
"endian": "little",
1404+
"system": "android",
1405+
}
1406+
}
1407+
1408+
def write_build_options(self, arch):
1409+
option_data = ""
1410+
build_options = self.get_recipe_meson_options(arch)
1411+
for key in build_options.keys():
1412+
data_chunk = "[{}]".format(key)
1413+
for subkey in build_options[key].keys():
1414+
value = build_options[key][subkey]
1415+
if isinstance(value, int):
1416+
value = str(value)
1417+
elif isinstance(value, str):
1418+
value = "'{}'".format(value)
1419+
elif isinstance(value, bool):
1420+
value = "true" if value else "false"
1421+
elif isinstance(value, list):
1422+
value = "['" + "', '".join(value) + "']"
1423+
data_chunk += "\n" + subkey + " = " + value
1424+
option_data += data_chunk + "\n\n"
1425+
return option_data
1426+
1427+
def ensure_args(self, *args):
1428+
for arg in args:
1429+
if arg not in self.extra_build_args:
1430+
self.extra_build_args.append(arg)
1431+
1432+
def build_arch(self, arch):
1433+
cross_file = join(self.get_build_dir(arch), "android.meson.cross")
1434+
info("Writing cross file at: {}".format(cross_file))
1435+
# write cross config file
1436+
with open(cross_file, "w") as file:
1437+
file.write(self.write_build_options(arch))
1438+
file.close()
1439+
# set cross file
1440+
self.ensure_args('-Csetup-args=--cross-file', '-Csetup-args={}'.format(cross_file))
1441+
# ensure ninja
1442+
if "ninja" not in self.hostpython_prerequisites:
1443+
self.hostpython_prerequisites.append("ninja")
1444+
super().build_arch(arch)
1445+
1446+
12981447
class TargetPythonRecipe(Recipe):
12991448
'''Class for target python recipes. Sets ctx.python_recipe to point to
13001449
itself, so as to know later what kind of Python was built or used.'''

pythonforandroid/recipes/matplotlib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class MatplotlibRecipe(CppCompiledComponentsPythonRecipe):
99

10-
version = '3.5.2'
10+
version = '3.8.4'
1111
url = 'https://github.com/matplotlib/matplotlib/archive/v{version}.zip'
1212

1313
depends = ['kiwisolver', 'numpy', 'pillow', 'setuptools', 'freetype']

pythonforandroid/recipes/numpy/__init__.py

Lines changed: 15 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,18 @@
1-
from pythonforandroid.recipe import CompiledComponentsPythonRecipe, Recipe
2-
from pythonforandroid.logger import shprint, info
3-
from pythonforandroid.util import current_directory
4-
from multiprocessing import cpu_count
1+
from pythonforandroid.recipe import Recipe, MesonRecipe
52
from os.path import join
6-
import glob
7-
import sh
83
import shutil
94

105

11-
class NumpyRecipe(CompiledComponentsPythonRecipe):
6+
class NumpyRecipe(MesonRecipe):
7+
version = 'e59c074842e3f73483afa5ddef031e856b9fd313' # 2.0.0
8+
url = 'git+https://github.com/numpy/numpy'
9+
hostpython_prerequisites = ["Cython>=3.0.6", "meson-python>=0.15.0"]
10+
extra_build_args = ['-Csetup-args=-Dblas=none', '-Csetup-args=-Dlapack=none']
1211

13-
version = '1.22.3'
14-
url = 'https://pypi.python.org/packages/source/n/numpy/numpy-{version}.zip'
15-
site_packages_name = 'numpy'
16-
depends = ["cython"]
17-
18-
# This build specifically requires setuptools version 59.2.0
19-
hostpython_prerequisites = ["setuptools==59.2.0"]
20-
21-
install_in_hostpython = True
22-
call_hostpython_via_targetpython = False
23-
24-
patches = [
25-
join("patches", "remove-default-paths.patch"),
26-
join("patches", "add_libm_explicitly_to_build.patch"),
27-
join("patches", "ranlib.patch"),
28-
]
12+
def get_recipe_meson_options(self, arch):
13+
options = super().get_recipe_meson_options(arch)
14+
options["properties"]["longdouble_format"] = "IEEE_DOUBLE_LE" if arch.arch in ["armeabi-v7a", "x86"] else "IEEE_QUAD_LE"
15+
return options
2916

3017
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
3118
env = super().get_recipe_env(arch, with_flags_in_cc)
@@ -37,54 +24,19 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True):
3724
# NPY_DISABLE_SVML=1 allows numpy to build for non-AVX512 CPUs
3825
# See: https://github.com/numpy/numpy/issues/21196
3926
env["NPY_DISABLE_SVML"] = "1"
40-
27+
env["TARGET_PYTHON_EXE"] = join(Recipe.get_recipe(
28+
"python3", self.ctx).get_build_dir(arch.arch), "android-build", "python")
4129
return env
4230

4331
def build_arch(self, arch):
44-
self.hostpython_prerequisites = ["setuptools==59.2.0"]
45-
self.install_hostpython_prerequisites()
46-
4732
super().build_arch(arch)
48-
49-
# Post build step to restore setuptools version
50-
self.hostpython_prerequisites = ["setuptools=={}".format(
51-
Recipe.get_recipe("setuptools", self.ctx).version)
52-
]
53-
self.install_hostpython_prerequisites()
54-
55-
def _build_compiled_components(self, arch):
56-
info('Building compiled components in {}'.format(self.name))
57-
58-
env = self.get_recipe_env(arch)
59-
with current_directory(self.get_build_dir(arch.arch)):
60-
hostpython = sh.Command(self.hostpython_location)
61-
shprint(hostpython, 'setup.py', self.build_cmd, '-v',
62-
_env=env, *self.setup_extra_args)
63-
build_dir = glob.glob('build/lib.*')[0]
64-
shprint(sh.find, build_dir, '-name', '"*.o"', '-exec',
65-
env['STRIP'], '{}', ';', _env=env)
66-
67-
def _rebuild_compiled_components(self, arch, env):
68-
info('Rebuilding compiled components in {}'.format(self.name))
69-
70-
hostpython = sh.Command(self.real_hostpython_location)
71-
shprint(hostpython, 'setup.py', 'clean', '--all', '--force', _env=env)
72-
shprint(hostpython, 'setup.py', self.build_cmd, '-v', _env=env,
73-
*self.setup_extra_args)
74-
75-
def build_compiled_components(self, arch):
76-
self.setup_extra_args = ['-j', str(cpu_count())]
77-
self._build_compiled_components(arch)
78-
self.setup_extra_args = []
79-
80-
def rebuild_compiled_components(self, arch, env):
81-
self.setup_extra_args = ['-j', str(cpu_count())]
82-
self._rebuild_compiled_components(arch, env)
83-
self.setup_extra_args = []
33+
# restore cython version
34+
self.restore_hostpython_prerequisites("cython")
8435

8536
def get_hostrecipe_env(self, arch):
8637
env = super().get_hostrecipe_env(arch)
8738
env['RANLIB'] = shutil.which('ranlib')
39+
env["LDFLAGS"] += " -lm"
8840
return env
8941

9042

pythonforandroid/recipes/numpy/patches/add_libm_explicitly_to_build.patch

Lines changed: 0 additions & 20 deletions
This file was deleted.

pythonforandroid/recipes/numpy/patches/ranlib.patch

Lines changed: 0 additions & 11 deletions
This file was deleted.

pythonforandroid/recipes/numpy/patches/remove-default-paths.patch

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)