Skip to content

Commit 2ac6d8e

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

File tree

18 files changed

+311
-289
lines changed

18 files changed

+311
-289
lines changed

Dockerfile

Lines changed: 2 additions & 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++ \
@@ -70,6 +71,7 @@ RUN ${RETRY} apt -y update -qq > /dev/null \
7071
make \
7172
openjdk-17-jdk \
7273
patch \
74+
patchelf \
7375
pkg-config \
7476
python3 \
7577
python3-dev \

ci/osx_ci.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ arm64_set_path_and_python_version(){
55
python_version="$1"
66
if [[ $(/usr/bin/arch) = arm64 ]]; then
77
export PATH=/opt/homebrew/bin:$PATH
8+
brew install pyenv
89
eval "$(pyenv init --path)"
910
pyenv install $python_version -s
1011
pyenv global $python_version
1112
export PATH=$(pyenv prefix)/bin:$PATH
1213
fi
13-
}
14+
}

doc/source/quickstart.rst

Lines changed: 2 additions & 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++ \
@@ -85,6 +86,7 @@ the following command (re-adapted from the `Dockerfile` we use to perform CI bui
8586
make \
8687
openjdk-17-jdk \
8788
patch \
89+
patchelf \
8890
pkg-config \
8991
python3 \
9092
python3-dev \

pythonforandroid/recipe.py

Lines changed: 179 additions & 90 deletions
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, sep
1+
from os.path import basename, dirname, exists, isdir, isfile, join, realpath, split
22
import glob
33

44
import hashlib
@@ -478,10 +478,10 @@ def unpack(self, arch):
478478
elif isdir(extraction_filename):
479479
ensure_dir(directory_name)
480480
for entry in listdir(extraction_filename):
481-
if entry not in ('.git',):
482-
shprint(sh.cp, '-Rv',
483-
join(extraction_filename, entry),
484-
directory_name)
481+
# if entry not in ('.git',): .git folder is required by `setuptools_scm`
482+
shprint(sh.cp, '-Rv',
483+
join(extraction_filename, entry),
484+
directory_name)
485485
else:
486486
raise Exception(
487487
'Given path is neither a file nor a directory: {}'
@@ -843,7 +843,6 @@ class PythonRecipe(Recipe):
843843

844844
def __init__(self, *args, **kwargs):
845845
super().__init__(*args, **kwargs)
846-
847846
if 'python3' not in self.depends:
848847
# We ensure here that the recipe depends on python even it overrode
849848
# `depends`. We only do this if it doesn't already depend on any
@@ -893,12 +892,12 @@ def folder_name(self):
893892

894893
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
895894
env = super().get_recipe_env(arch, with_flags_in_cc)
896-
897895
env['PYTHONNOUSERSITE'] = '1'
898-
899896
# Set the LANG, this isn't usually important but is a better default
900897
# as it occasionally matters how Python e.g. reads files
901898
env['LANG'] = "en_GB.UTF-8"
899+
# Binaries made by packages installed by pip
900+
env["PATH"] = join(self.hostpython_site_dir, "bin") + ":" + env["PATH"]
902901

903902
if not self.call_hostpython_via_targetpython:
904903
env['CFLAGS'] += ' -I{}'.format(
@@ -978,27 +977,35 @@ def install_hostpython_package(self, arch):
978977
'--install-lib=Lib/site-packages',
979978
_env=env, *self.setup_extra_args)
980979

981-
@property
982-
def python_version(self):
983-
return Recipe.get_recipe("python3", self.ctx).version
980+
def get_python_formatted_version(self):
981+
parsed_version = packaging.version.parse(self.ctx.python_recipe.version)
982+
return f"{parsed_version.major}.{parsed_version.minor}"
983+
984+
def install_hostpython_prerequisites(self, packages=None, force_upgrade=True):
985+
if not packages:
986+
packages = self.hostpython_prerequisites
984987

985-
def install_hostpython_prerequisites(self, force_upgrade=True):
986-
if len(self.hostpython_prerequisites) == 0:
988+
if len(packages) == 0:
987989
return
990+
988991
pip_options = [
989992
"install",
990-
*self.hostpython_prerequisites,
993+
*packages,
991994
"--target", self.hostpython_site_dir, "--python-version",
992-
self.python_version,
995+
self.ctx.python_recipe.version,
993996
# Don't use sources, instead wheels
994997
"--only-binary=:all:",
995-
"--no-deps"
998+
# "--no-deps"
996999
]
9971000
if force_upgrade:
9981001
pip_options.append("--upgrade")
9991002
# Use system's pip
10001003
shprint(sh.pip, *pip_options)
10011004

1005+
def restore_hostpython_prerequisites(self, package):
1006+
original_version = Recipe.get_recipe(package, self.ctx).version
1007+
self.install_hostpython_prerequisites(packages=[package + "==" + original_version])
1008+
10021009

10031010
class CompiledComponentsPythonRecipe(PythonRecipe):
10041011
pre_build_ext = False
@@ -1157,7 +1164,158 @@ def get_recipe_env(self, arch, with_flags_in_cc=True):
11571164
return env
11581165

11591166

1160-
class RustCompiledComponentsRecipe(PythonRecipe):
1167+
class PyProjectRecipe(PythonRecipe):
1168+
'''Recipe for projects which containes `pyproject.toml`'''
1169+
1170+
# Extra args to pass to `python -m build ...`
1171+
extra_build_args = []
1172+
call_hostpython_via_targetpython = False
1173+
1174+
def __init__(self, *arg, **kwargs):
1175+
super().__init__(*arg, **kwargs)
1176+
1177+
def get_recipe_env(self, arch, **kwargs):
1178+
self.ctx.python_recipe.python_exe = join(
1179+
self.ctx.python_recipe.get_build_dir(arch), "android-build", "python3")
1180+
env = super().get_recipe_env(arch, **kwargs)
1181+
build_dir = self.get_build_dir(arch)
1182+
ensure_dir(build_dir)
1183+
build_opts = join(build_dir, "build-opts.cfg")
1184+
1185+
with open(build_opts, "w") as file:
1186+
file.write("[bdist_wheel]\nplat-name=android_{}".format(str(arch)))
1187+
file.close()
1188+
1189+
env["DIST_EXTRA_CONFIG"] = build_opts
1190+
return env
1191+
1192+
def build_arch(self, arch):
1193+
self.install_hostpython_prerequisites(
1194+
packages=["build[virtualenv]", "pip"] + self.hostpython_prerequisites
1195+
)
1196+
build_dir = self.get_build_dir(arch.arch)
1197+
env = self.get_recipe_env(arch, with_flags_in_cc=True)
1198+
built_wheel = None
1199+
# make build dir separatly
1200+
sub_build_dir = join(build_dir, "p4a_android_build")
1201+
ensure_dir(sub_build_dir)
1202+
# copy hostpython to built python to ensure correct libs and includes
1203+
shprint(sh.cp, self.real_hostpython_location, self.ctx.python_recipe.python_exe)
1204+
1205+
build_args = [
1206+
"-m",
1207+
"build",
1208+
# "--no-isolation",
1209+
# "--skip-dependency-check",
1210+
"--wheel",
1211+
"--config-setting",
1212+
"builddir={}".format(sub_build_dir),
1213+
] + self.extra_build_args
1214+
1215+
with current_directory(build_dir):
1216+
shprint(
1217+
sh.Command(self.ctx.python_recipe.python_exe), *build_args, _env=env
1218+
)
1219+
built_wheel = realpath(glob.glob("dist/*.whl")[0])
1220+
1221+
info("Unzipping built wheel '{}'".format(basename(built_wheel)))
1222+
1223+
with zipfile.ZipFile(built_wheel, "r") as zip_ref:
1224+
zip_ref.extractall(self.ctx.get_python_install_dir(arch.arch))
1225+
info("Successfully installed '{}'".format(basename(built_wheel)))
1226+
1227+
1228+
class MesonRecipe(PyProjectRecipe):
1229+
'''Recipe for projects which uses meson as build system'''
1230+
1231+
meson_version = "1.4.0"
1232+
ninja_version = "1.11.1.1"
1233+
1234+
def sanitize_flags(self, *flag_strings):
1235+
return " ".join(flag_strings).strip().split(" ")
1236+
1237+
def get_recipe_meson_options(self, arch):
1238+
"""Writes python dict to meson config file"""
1239+
env = self.get_recipe_env(arch, with_flags_in_cc=True)
1240+
return {
1241+
"binaries": {
1242+
"c": arch.get_clang_exe(with_target=True),
1243+
"cpp": arch.get_clang_exe(with_target=True, plus_plus=True),
1244+
"ar": self.ctx.ndk.llvm_ar,
1245+
"strip": self.ctx.ndk.llvm_strip,
1246+
},
1247+
"built-in options": {
1248+
"c_args": self.sanitize_flags(env["CFLAGS"], env["CPPFLAGS"]),
1249+
"cpp_args": self.sanitize_flags(env["CXXFLAGS"], env["CPPFLAGS"]),
1250+
"c_link_args": self.sanitize_flags(env["LDFLAGS"]),
1251+
"cpp_link_args": self.sanitize_flags(env["LDFLAGS"]),
1252+
},
1253+
"properties": {
1254+
"needs_exe_wrapper": True,
1255+
"sys_root": self.ctx.ndk.sysroot
1256+
},
1257+
"host_machine": {
1258+
"cpu_family": {
1259+
"arm64-v8a": "aarch64",
1260+
"armeabi-v7a": "arm",
1261+
"x86_64": "x86_64",
1262+
"x86": "x86"
1263+
}[arch.arch],
1264+
"cpu": {
1265+
"arm64-v8a": "aarch64",
1266+
"armeabi-v7a": "armv7",
1267+
"x86_64": "x86_64",
1268+
"x86": "i686"
1269+
}[arch.arch],
1270+
"endian": "little",
1271+
"system": "android",
1272+
}
1273+
}
1274+
1275+
def write_build_options(self, arch):
1276+
option_data = ""
1277+
build_options = self.get_recipe_meson_options(arch)
1278+
for key in build_options.keys():
1279+
data_chunk = "[{}]".format(key)
1280+
for subkey in build_options[key].keys():
1281+
value = build_options[key][subkey]
1282+
if isinstance(value, int):
1283+
value = str(value)
1284+
elif isinstance(value, str):
1285+
value = "'{}'".format(value)
1286+
elif isinstance(value, bool):
1287+
value = "true" if value else "false"
1288+
elif isinstance(value, list):
1289+
value = "['" + "', '".join(value) + "']"
1290+
data_chunk += "\n" + subkey + " = " + value
1291+
option_data += data_chunk + "\n\n"
1292+
return option_data
1293+
1294+
def ensure_args(self, *args):
1295+
for arg in args:
1296+
if arg not in self.extra_build_args:
1297+
self.extra_build_args.append(arg)
1298+
1299+
def build_arch(self, arch):
1300+
cross_file = join(self.get_build_dir(arch), "android.meson.cross")
1301+
info("Writing cross file at: {}".format(cross_file))
1302+
# write cross config file
1303+
with open(cross_file, "w") as file:
1304+
file.write(self.write_build_options(arch))
1305+
file.close()
1306+
# set cross file
1307+
self.ensure_args('-Csetup-args=--cross-file', '-Csetup-args={}'.format(cross_file))
1308+
# ensure ninja and meson
1309+
for dep in [
1310+
"ninja=={}".format(self.ninja_version),
1311+
"meson=={}".format(self.meson_version),
1312+
]:
1313+
if dep not in self.hostpython_prerequisites:
1314+
self.hostpython_prerequisites.append(dep)
1315+
super().build_arch(arch)
1316+
1317+
1318+
class RustCompiledComponentsRecipe(PyProjectRecipe):
11611319
# Rust toolchain codes
11621320
# https://doc.rust-lang.org/nightly/rustc/platform-support.html
11631321
RUST_ARCH_CODES = {
@@ -1167,41 +1325,10 @@ class RustCompiledComponentsRecipe(PythonRecipe):
11671325
"x86": "i686-linux-android",
11681326
}
11691327

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-
11791328
call_hostpython_via_targetpython = False
11801329

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)
1330+
def get_recipe_env(self, arch, **kwargs):
1331+
env = super().get_recipe_env(arch, **kwargs)
12051332

12061333
# Set rust build target
12071334
build_target = self.RUST_ARCH_CODES[arch.arch]
@@ -1220,7 +1347,7 @@ def get_recipe_env(self, arch):
12201347
self.ctx.ndk_api,
12211348
),
12221349
)
1223-
realpython_dir = Recipe.get_recipe("python3", self.ctx).get_build_dir(arch.arch)
1350+
realpython_dir = self.ctx.python_recipe.get_build_dir(arch.arch)
12241351

12251352
env["RUSTFLAGS"] = "-Clink-args=-L{} -L{}".format(
12261353
self.ctx.get_libs_dir(arch.arch), join(realpython_dir, "android-build")
@@ -1243,10 +1370,6 @@ def get_recipe_env(self, arch):
12431370
)
12441371
return env
12451372

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-
12501373
def check_host_deps(self):
12511374
if not hasattr(sh, "rustup"):
12521375
error(
@@ -1258,41 +1381,7 @@ def check_host_deps(self):
12581381

12591382
def build_arch(self, arch):
12601383
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)))
1384+
super().build_arch(arch)
12961385

12971386

12981387
class TargetPythonRecipe(Recipe):

0 commit comments

Comments
 (0)