Skip to content

Commit 8110faf

Browse files
authored
recipes: Introduce RustCompiledComponentsRecipe, add pydantic-core and update cryptography (#2962)
1 parent c67faf6 commit 8110faf

File tree

7 files changed

+221
-30
lines changed

7 files changed

+221
-30
lines changed

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ virtualenv: $(VIRTUAL_ENV)
2323
test:
2424
$(TOX) -- tests/ --ignore tests/test_pythonpackage.py
2525

26+
# Also install and configure rust
2627
rebuild_updated_recipes: virtualenv
2728
. $(ACTIVATE) && \
29+
curl https://sh.rustup.rs -sSf | sh -s -- -y && \
30+
. "$(HOME)/.cargo/env" && \
31+
rustup target list && \
2832
ANDROID_SDK_HOME=$(ANDROID_SDK_HOME) ANDROID_NDK_HOME=$(ANDROID_NDK_HOME) \
2933
$(PYTHON) ci/rebuild_updated_recipes.py $(REBUILD_UPDATED_RECIPES_EXTRA_ARGS)
3034

pythonforandroid/recipe.py

+172-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from os.path import basename, dirname, exists, isdir, isfile, join, realpath, split
1+
from os.path import basename, dirname, exists, isdir, isfile, join, realpath, split, sep
22
import glob
33

44
import hashlib
@@ -7,6 +7,7 @@
77
import sh
88
import shutil
99
import fnmatch
10+
import zipfile
1011
import urllib.request
1112
from urllib.request import urlretrieve
1213
from os import listdir, unlink, environ, curdir, walk
@@ -20,7 +21,7 @@
2021
import packaging.version
2122

2223
from pythonforandroid.logger import (
23-
logger, info, warning, debug, shprint, info_main)
24+
logger, info, warning, debug, shprint, info_main, error)
2425
from pythonforandroid.util import (
2526
current_directory, ensure_dir, BuildInterruptingException, rmdir, move,
2627
touch)
@@ -175,6 +176,7 @@ def download_file(self, url, target, cwd=None):
175176
"""
176177
if not url:
177178
return
179+
178180
info('Downloading {} from {}'.format(self.name, url))
179181

180182
if cwd:
@@ -458,7 +460,6 @@ def unpack(self, arch):
458460
# apparently happens sometimes with
459461
# github zips
460462
pass
461-
import zipfile
462463
fileh = zipfile.ZipFile(extraction_filename, 'r')
463464
root_directory = fileh.filelist[0].filename.split('/')[0]
464465
if root_directory != basename(directory_name):
@@ -837,6 +838,9 @@ class PythonRecipe(Recipe):
837838
on python2 or python3 which can break the dependency graph
838839
'''
839840

841+
hostpython_prerequisites = []
842+
'''List of hostpython packages required to build a recipe'''
843+
840844
def __init__(self, *args, **kwargs):
841845
super().__init__(*args, **kwargs)
842846

@@ -930,6 +934,7 @@ def should_build(self, arch):
930934
def build_arch(self, arch):
931935
'''Install the Python module by calling setup.py install with
932936
the target Python dir.'''
937+
self.install_hostpython_prerequisites()
933938
super().build_arch(arch)
934939
self.install_python_package(arch)
935940

@@ -958,9 +963,13 @@ def install_python_package(self, arch, name=None, env=None, is_dir=True):
958963

959964
def get_hostrecipe_env(self, arch):
960965
env = environ.copy()
961-
env['PYTHONPATH'] = join(dirname(self.real_hostpython_location), 'Lib', 'site-packages')
966+
env['PYTHONPATH'] = self.hostpython_site_dir
962967
return env
963968

969+
@property
970+
def hostpython_site_dir(self):
971+
return join(dirname(self.real_hostpython_location), 'Lib', 'site-packages')
972+
964973
def install_hostpython_package(self, arch):
965974
env = self.get_hostrecipe_env(arch)
966975
real_hostpython = sh.Command(self.real_hostpython_location)
@@ -969,6 +978,27 @@ def install_hostpython_package(self, arch):
969978
'--install-lib=Lib/site-packages',
970979
_env=env, *self.setup_extra_args)
971980

981+
@property
982+
def python_version(self):
983+
return Recipe.get_recipe("python3", self.ctx).version
984+
985+
def install_hostpython_prerequisites(self, force_upgrade=True):
986+
if len(self.hostpython_prerequisites) == 0:
987+
return
988+
pip_options = [
989+
"install",
990+
*self.hostpython_prerequisites,
991+
"--target", self.hostpython_site_dir, "--python-version",
992+
self.python_version,
993+
# Don't use sources, instead wheels
994+
"--only-binary=:all:",
995+
"--no-deps"
996+
]
997+
if force_upgrade:
998+
pip_options.append("--upgrade")
999+
# Use system's pip
1000+
shprint(sh.pip, *pip_options)
1001+
9721002

9731003
class CompiledComponentsPythonRecipe(PythonRecipe):
9741004
pre_build_ext = False
@@ -1127,6 +1157,144 @@ def get_recipe_env(self, arch, with_flags_in_cc=True):
11271157
return env
11281158

11291159

1160+
class RustCompiledComponentsRecipe(PythonRecipe):
1161+
# Rust toolchain codes
1162+
# https://doc.rust-lang.org/nightly/rustc/platform-support.html
1163+
RUST_ARCH_CODES = {
1164+
"arm64-v8a": "aarch64-linux-android",
1165+
"armeabi-v7a": "armv7-linux-androideabi",
1166+
"x86_64": "x86_64-linux-android",
1167+
"x86": "i686-linux-android",
1168+
}
1169+
1170+
# Build python wheel using `maturin` instead
1171+
# of default `python -m build [...]`
1172+
use_maturin = False
1173+
1174+
# Directory where to find built wheel
1175+
# For normal build: "dist/*.whl"
1176+
# For maturin: "target/wheels/*-linux_*.whl"
1177+
built_wheel_pattern = None
1178+
1179+
call_hostpython_via_targetpython = False
1180+
1181+
def __init__(self, *arg, **kwargs):
1182+
super().__init__(*arg, **kwargs)
1183+
self.append_deps_if_absent(["python3"])
1184+
self.set_default_hostpython_deps()
1185+
if not self.built_wheel_pattern:
1186+
self.built_wheel_pattern = (
1187+
"target/wheels/*-linux_*.whl"
1188+
if self.use_maturin
1189+
else "dist/*.whl"
1190+
)
1191+
1192+
def set_default_hostpython_deps(self):
1193+
if not self.use_maturin:
1194+
self.hostpython_prerequisites += ["build", "setuptools_rust", "wheel", "pyproject_hooks"]
1195+
else:
1196+
self.hostpython_prerequisites += ["maturin"]
1197+
1198+
def append_deps_if_absent(self, deps):
1199+
for dep in deps:
1200+
if dep not in self.depends:
1201+
self.depends.append(dep)
1202+
1203+
def get_recipe_env(self, arch):
1204+
env = super().get_recipe_env(arch)
1205+
1206+
# Set rust build target
1207+
build_target = self.RUST_ARCH_CODES[arch.arch]
1208+
cargo_linker_name = "CARGO_TARGET_{}_LINKER".format(
1209+
build_target.upper().replace("-", "_")
1210+
)
1211+
env["CARGO_BUILD_TARGET"] = build_target
1212+
env[cargo_linker_name] = join(
1213+
self.ctx.ndk.llvm_prebuilt_dir,
1214+
"bin",
1215+
"{}{}-clang".format(
1216+
# NDK's Clang format
1217+
build_target.replace("7", "7a")
1218+
if build_target.startswith("armv7")
1219+
else build_target,
1220+
self.ctx.ndk_api,
1221+
),
1222+
)
1223+
realpython_dir = Recipe.get_recipe("python3", self.ctx).get_build_dir(arch.arch)
1224+
1225+
env["RUSTFLAGS"] = "-Clink-args=-L{} -L{}".format(
1226+
self.ctx.get_libs_dir(arch.arch), join(realpython_dir, "android-build")
1227+
)
1228+
1229+
env["PYO3_CROSS_LIB_DIR"] = realpath(glob.glob(join(
1230+
realpython_dir, "android-build", "build",
1231+
"lib.linux-*-{}/".format(self.get_python_formatted_version()),
1232+
))[0])
1233+
1234+
info_main("Ensuring rust build toolchain")
1235+
shprint(sh.rustup, "target", "add", build_target)
1236+
1237+
# Add host python to PATH
1238+
env["PATH"] = ("{hostpython_dir}:{old_path}").format(
1239+
hostpython_dir=Recipe.get_recipe(
1240+
"hostpython3", self.ctx
1241+
).get_path_to_python(),
1242+
old_path=env["PATH"],
1243+
)
1244+
return env
1245+
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+
1250+
def check_host_deps(self):
1251+
if not hasattr(sh, "rustup"):
1252+
error(
1253+
"`rustup` was not found on host system."
1254+
"Please install it using :"
1255+
"\n`curl https://sh.rustup.rs -sSf | sh`\n"
1256+
)
1257+
exit(1)
1258+
1259+
def build_arch(self, arch):
1260+
self.check_host_deps()
1261+
self.install_hostpython_prerequisites()
1262+
build_dir = self.get_build_dir(arch.arch)
1263+
env = self.get_recipe_env(arch)
1264+
built_wheel = None
1265+
1266+
# Copy the exec with version info
1267+
hostpython_exec = join(
1268+
sep,
1269+
*self.hostpython_location.split(sep)[:-1],
1270+
"python{}".format(self.get_python_formatted_version()),
1271+
)
1272+
shprint(sh.cp, self.hostpython_location, hostpython_exec)
1273+
1274+
with current_directory(build_dir):
1275+
if self.use_maturin:
1276+
shprint(
1277+
sh.Command(join(self.hostpython_site_dir, "bin", "maturin")),
1278+
"build", "--interpreter", hostpython_exec, "--skip-auditwheel",
1279+
_env=env,
1280+
)
1281+
else:
1282+
shprint(
1283+
sh.Command(hostpython_exec),
1284+
"-m", "build", "--no-isolation", "--skip-dependency-check", "--wheel",
1285+
_env=env,
1286+
)
1287+
# Find the built wheel
1288+
built_wheel = realpath(glob.glob(self.built_wheel_pattern)[0])
1289+
1290+
info("Unzipping built wheel '{}'".format(basename(built_wheel)))
1291+
1292+
# Unzip .whl file into site-packages
1293+
with zipfile.ZipFile(built_wheel, "r") as zip_ref:
1294+
zip_ref.extractall(self.ctx.get_python_install_dir(arch.arch))
1295+
info("Successfully installed '{}'".format(basename(built_wheel)))
1296+
1297+
11301298
class TargetPythonRecipe(Recipe):
11311299
'''Class for target python recipes. Sets ctx.python_recipe to point to
11321300
itself, so as to know later what kind of Python was built or used.'''

pythonforandroid/recipes/cryptography/__init__.py

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
1-
from pythonforandroid.recipe import CompiledComponentsPythonRecipe, Recipe
1+
from pythonforandroid.recipe import RustCompiledComponentsRecipe
2+
from os.path import join
23

34

4-
class CryptographyRecipe(CompiledComponentsPythonRecipe):
5+
class CryptographyRecipe(RustCompiledComponentsRecipe):
6+
57
name = 'cryptography'
6-
version = '2.8'
7-
url = 'https://github.com/pyca/cryptography/archive/{version}.tar.gz'
8+
version = '42.0.1'
9+
url = 'https://github.com/pyca/cryptography/archive/refs/tags/{version}.tar.gz'
810
depends = ['openssl', 'six', 'setuptools', 'cffi']
9-
call_hostpython_via_targetpython = False
11+
# recipe built cffi does not work on apple M1
12+
hostpython_prerequisites = ["semantic_version", "cffi"]
1013

1114
def get_recipe_env(self, arch):
1215
env = super().get_recipe_env(arch)
13-
14-
openssl_recipe = Recipe.get_recipe('openssl', self.ctx)
15-
env['CFLAGS'] += openssl_recipe.include_flags(arch)
16-
env['LDFLAGS'] += openssl_recipe.link_dirs_flags(arch)
17-
env['LIBS'] = openssl_recipe.link_libs_flags()
18-
16+
openssl_build_dir = self.get_recipe('openssl', self.ctx).get_build_dir(arch.arch)
17+
build_target = self.RUST_ARCH_CODES[arch.arch].upper().replace("-", "_")
18+
openssl_include = "{}_OPENSSL_INCLUDE_DIR".format(build_target)
19+
openssl_libs = "{}_OPENSSL_LIB_DIR".format(build_target)
20+
env[openssl_include] = join(openssl_build_dir, 'include')
21+
env[openssl_libs] = join(openssl_build_dir)
1922
return env
2023

2124

pythonforandroid/recipes/numpy/__init__.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pythonforandroid.recipe import CompiledComponentsPythonRecipe
1+
from pythonforandroid.recipe import CompiledComponentsPythonRecipe, Recipe
22
from pythonforandroid.logger import shprint, info
33
from pythonforandroid.util import current_directory
44
from multiprocessing import cpu_count
@@ -13,7 +13,11 @@ class NumpyRecipe(CompiledComponentsPythonRecipe):
1313
version = '1.22.3'
1414
url = 'https://pypi.python.org/packages/source/n/numpy/numpy-{version}.zip'
1515
site_packages_name = 'numpy'
16-
depends = ['setuptools', 'cython']
16+
depends = ["cython"]
17+
18+
# This build specifically requires setuptools version 59.2.0
19+
hostpython_prerequisites = ["setuptools==59.2.0"]
20+
1721
install_in_hostpython = True
1822
call_hostpython_via_targetpython = False
1923

@@ -36,6 +40,18 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True):
3640

3741
return env
3842

43+
def build_arch(self, arch):
44+
self.hostpython_prerequisites = ["setuptools==59.2.0"]
45+
self.install_hostpython_prerequisites()
46+
47+
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+
3955
def _build_compiled_components(self, arch):
4056
info('Building compiled components in {}'.format(self.name))
4157

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pythonforandroid.recipe import RustCompiledComponentsRecipe
2+
3+
4+
class PydanticcoreRecipe(RustCompiledComponentsRecipe):
5+
version = "2.16.1"
6+
url = "https://github.com/pydantic/pydantic-core/archive/refs/tags/v{version}.tar.gz"
7+
use_maturin = True
8+
hostpython_prerequisites = ["typing_extensions"]
9+
site_packages_name = "pydantic_core"
10+
11+
12+
recipe = PydanticcoreRecipe()

pythonforandroid/recipes/pydantic/__init__.py

-12
This file was deleted.

pythonforandroid/recipes/setuptools/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
class SetuptoolsRecipe(PythonRecipe):
5-
version = '51.3.3'
5+
version = '69.2.0'
66
url = 'https://pypi.python.org/packages/source/s/setuptools/setuptools-{version}.tar.gz'
77
call_hostpython_via_targetpython = False
88
install_in_hostpython = True

0 commit comments

Comments
 (0)