Skip to content

python: update to 3.13 #3082

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions pythonforandroid/artifact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import zipfile
import json
import os
import subprocess


class ArtifactName:

platform = "android"

def __init__(self, recipe, arch):
self.recipe = recipe
self._arch = arch

@property
def stage(self):
return "master"
result = subprocess.check_output(
["git", "branch", "--show-current"],
stderr=subprocess.PIPE,
universal_newlines=True,
)
return result.strip()

@property
def kind(self):
return "lib"

@property
def arch(self):
return self._arch.arch

@property
def native_api_level(self):
return str(self.recipe.ctx.ndk_api)

@property
def file_props(self):
return [
self.stage,
self.kind,
self.recipe.name,
self.arch,
self.platform + self.native_api_level,
self.recipe.version,
]

@property
def filename(self):
return "_".join(self.file_props) + ".zip"


def build_artifact(
save_path,
recipe,
arch,
lib_dependencies=[],
files_dependencies=[],
install_instructions=[],
):
# Parse artifact name
artifact_name = ArtifactName(recipe, arch)
zip_path = os.path.join(save_path, artifact_name.filename)

# Contents of zip file
metadata_folder = "metadata/"
data_folder = "data/"
prop_file = os.path.join(metadata_folder, "properties.json")
install_file = os.path.join(metadata_folder, "install.json")

properties = {
"stage": artifact_name.stage,
"name": recipe.name,
"arch": artifact_name.arch,
"native_api_level": artifact_name.native_api_level,
"kind": artifact_name.kind,
"version": recipe.version,
"release_version": recipe.version,
"lib_dependencies": lib_dependencies,
"files_dependencies": files_dependencies,
}

with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
zipf.writestr(metadata_folder, "")
zipf.writestr(data_folder, "")

for file_name in lib_dependencies + files_dependencies:
with open(file_name, "rb") as file:
file_name_ = os.path.join(data_folder + os.path.basename(file_name))
zipf.writestr(file_name_, file.read())
file.close()

zipf.writestr(prop_file, json.dumps(properties))
zipf.writestr(install_file, json.dumps(install_instructions))
zipf.close()
Original file line number Diff line number Diff line change
@@ -56,6 +56,8 @@ protected static ArrayList<String> getLibraries(File libsDir) {
libsList.add("python3.9");
libsList.add("python3.10");
libsList.add("python3.11");
libsList.add("python3.12");
libsList.add("python3.13");
libsList.add("main");
return libsList;
}
@@ -75,7 +77,7 @@ public static void loadLibraries(File filesDir, File libsDir) {
// load, and it has failed, give a more
// general error
Log.v(TAG, "Library loading error: " + e.getMessage());
if (lib.startsWith("python3.11") && !foundPython) {
if (lib.startsWith("python3.13") && !foundPython) {
throw new RuntimeException("Could not load any libpythonXXX.so");
} else if (lib.startsWith("python")) {
continue;
18 changes: 14 additions & 4 deletions pythonforandroid/build.py
Original file line number Diff line number Diff line change
@@ -94,6 +94,8 @@ class Context:

java_build_tool = 'auto'

save_prebuilt = False

@property
def packages_path(self):
'''Where packages are downloaded before being unpacked'''
@@ -147,11 +149,17 @@ def setup_dirs(self, storage_dir):
'specify a path with --storage-dir')
self.build_dir = join(self.storage_dir, 'build')
self.dist_dir = join(self.storage_dir, 'dists')
self.prebuilt_dir = join(self.storage_dir, 'output')
self.ensure_dirs()

def ensure_dirs(self):
ensure_dir(self.storage_dir)
ensure_dir(self.build_dir)
ensure_dir(self.dist_dir)

if self.save_prebuilt:
ensure_dir(self.prebuilt_dir)

ensure_dir(join(self.build_dir, 'bootstrap_builds'))
ensure_dir(join(self.build_dir, 'other_builds'))

@@ -219,8 +227,6 @@ def prepare_build_environment(self,

'''

self.ensure_dirs()

if self._build_env_prepared:
return

@@ -672,6 +678,7 @@ def run_pymodules_install(ctx, arch, modules, project_dir=None,
# Bail out if no python deps and no setup.py to process:
if not modules and (
ignore_setup_py or
project_dir is None or
not project_has_setup_py(project_dir)
):
info('No Python modules and no setup.py to process, skipping')
@@ -687,7 +694,8 @@ def run_pymodules_install(ctx, arch, modules, project_dir=None,
"If this fails, it may mean that the module has compiled "
"components and needs a recipe."
)
if project_has_setup_py(project_dir) and not ignore_setup_py:
if project_dir is not None and \
project_has_setup_py(project_dir) and not ignore_setup_py:
info(
"Will process project install, if it fails then the "
"project may not be compatible for Android install."
@@ -759,7 +767,9 @@ def run_pymodules_install(ctx, arch, modules, project_dir=None,
_env=copy.copy(env))

# Afterwards, run setup.py if present:
if project_has_setup_py(project_dir) and not ignore_setup_py:
if project_dir is not None and (
project_has_setup_py(project_dir) and not ignore_setup_py
):
run_setuppy_install(ctx, project_dir, env, arch)
elif not ignore_setup_py:
info("No setup.py found in project directory: " + str(project_dir))
6 changes: 3 additions & 3 deletions pythonforandroid/graph.py
Original file line number Diff line number Diff line change
@@ -240,7 +240,7 @@ def obvious_conflict_checker(ctx, name_tuples, blacklist=None):
return None


def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=None):
def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=None, should_log = False):
# Get set of recipe/dependency names, clean up and add bootstrap deps:
names = set(names)
if bs is not None and bs.recipe_depends:
@@ -311,7 +311,7 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=None):
for order in orders:
info(' {}'.format(order))
info('Using the first of these: {}'.format(chosen_order))
else:
elif should_log:
info('Found a single valid recipe set: {}'.format(chosen_order))

if bs is None:
@@ -322,7 +322,7 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=None):
"Could not find any compatible bootstrap!"
)
recipes, python_modules, bs = get_recipe_order_and_bootstrap(
ctx, chosen_order, bs=bs, blacklist=blacklist
ctx, chosen_order, bs=bs, blacklist=blacklist, should_log=True
)
else:
# check if each requirement has a recipe
7 changes: 4 additions & 3 deletions pythonforandroid/pythonpackage.py
Original file line number Diff line number Diff line change
@@ -556,10 +556,11 @@ def _extract_info_from_package(dependency,

# Get build requirements from pyproject.toml if requested:
requirements = []
pyproject_toml_path = os.path.join(output_folder, 'pyproject.toml')
if os.path.exists(pyproject_toml_path) and include_build_requirements:
if os.path.exists(os.path.join(output_folder,
'pyproject.toml')
) and include_build_requirements:
# Read build system from pyproject.toml file: (PEP518)
with open(pyproject_toml_path) as f:
with open(os.path.join(output_folder, 'pyproject.toml')) as f:
build_sys = toml.load(f)['build-system']
if "requires" in build_sys:
requirements += build_sys["requires"]
146 changes: 83 additions & 63 deletions pythonforandroid/recipe.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from os.path import basename, dirname, exists, isdir, isfile, join, realpath, split
import glob

import hashlib
import json
from re import match

import sh
@@ -12,6 +12,9 @@
from urllib.request import urlretrieve
from os import listdir, unlink, environ, curdir, walk
from sys import stdout
from multiprocessing import cpu_count
from wheel.wheelfile import WheelFile
from wheel.cli.tags import tags as wheel_tags
import time
try:
from urlparse import urlparse
@@ -24,8 +27,9 @@
logger, info, warning, debug, shprint, info_main, error)
from pythonforandroid.util import (
current_directory, ensure_dir, BuildInterruptingException, rmdir, move,
touch, patch_wheel_setuptools_logging)
touch)
from pythonforandroid.util import load_source as import_recipe
from pythonforandroid.artifact import build_artifact


url_opener = urllib.request.build_opener()
@@ -382,6 +386,10 @@ def download_if_necessary(self):
self.name, self.name))
return
self.download()

@property
def download_dir(self):
return self.name + "_" + self.version

def download(self):
if self.url is None:
@@ -403,9 +411,9 @@ def download(self):
if expected_digest:
expected_digests[alg] = expected_digest

ensure_dir(join(self.ctx.packages_path, self.name))
ensure_dir(join(self.ctx.packages_path, self.download_dir))

with current_directory(join(self.ctx.packages_path, self.name)):
with current_directory(join(self.ctx.packages_path, self.download_dir)):
filename = shprint(sh.basename, url).stdout[:-1].decode('utf-8')

do_download = True
@@ -479,7 +487,7 @@ def unpack(self, arch):

if not exists(directory_name) or not isdir(directory_name):
extraction_filename = join(
self.ctx.packages_path, self.name, filename)
self.ctx.packages_path, self.download_dir, filename)
if isfile(extraction_filename):
if extraction_filename.endswith(('.zip', '.whl')):
try:
@@ -509,7 +517,7 @@ def unpack(self, arch):
for entry in listdir(extraction_filename):
# Previously we filtered out the .git folder, but during the build process for some recipes
# (e.g. when version is parsed by `setuptools_scm`) that may be needed.
shprint(sh.cp, '-Rv',
shprint(sh.cp, '-R',
join(extraction_filename, entry),
directory_name)
else:
@@ -587,6 +595,7 @@ def build_arch(self, arch):
if hasattr(self, build):
getattr(self, build)()


def install_libraries(self, arch):
'''This method is always called after `build_arch`. In case that we
detect a library recipe, defined by the class attribute
@@ -595,9 +604,21 @@ def install_libraries(self, arch):
'''
if not self.built_libraries:
return

shared_libs = [
lib for lib in self.get_libraries(arch) if lib.endswith(".so")
]

if self.ctx.save_prebuilt:
build_artifact(
self.ctx.prebuilt_dir,
self,
arch,
shared_libs,
[],
[{"LIBSCOPY": [basename(lib) for lib in shared_libs]},]
)

self.install_libs(arch, *shared_libs)

def postbuild_arch(self, arch):
@@ -822,6 +843,8 @@ def build_arch(self, arch, *extra_args):
shprint(
sh.Command(join(self.ctx.ndk_dir, "ndk-build")),
'V=1',
"-j",
str(cpu_count()),
'NDK_DEBUG=' + ("1" if self.ctx.build_as_debuggable else "0"),
'APP_PLATFORM=android-' + str(self.ctx.ndk_api),
'APP_ABI=' + arch.arch,
@@ -869,7 +892,8 @@ class PythonRecipe(Recipe):

hostpython_prerequisites = []
'''List of hostpython packages required to build a recipe'''


_host_recipe = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'python3' not in self.depends:
@@ -882,6 +906,10 @@ def __init__(self, *args, **kwargs):
depends = list(set(depends))
self.depends = depends

def prebuild_arch(self, arch):
self._host_recipe = Recipe.get_recipe("hostpython3", self.ctx)
return super().prebuild_arch(arch)

def clean_build(self, arch=None):
super().clean_build(arch=arch)
name = self.folder_name
@@ -899,8 +927,7 @@ def clean_build(self, arch=None):
def real_hostpython_location(self):
host_name = 'host{}'.format(self.ctx.python_recipe.name)
if host_name == 'hostpython3':
python_recipe = Recipe.get_recipe(host_name, self.ctx)
return python_recipe.python_exe
return self._host_recipe.python_exe
else:
python_recipe = self.ctx.python_recipe
return 'python{}'.format(python_recipe.version)
@@ -920,13 +947,18 @@ def folder_name(self):
return name

def get_recipe_env(self, arch=None, with_flags_in_cc=True):
if self._host_recipe is None:
self._host_recipe = Recipe.get_recipe("hostpython3", self.ctx)

env = super().get_recipe_env(arch, with_flags_in_cc)
env['PYTHONNOUSERSITE'] = '1'
# Set the LANG, this isn't usually important but is a better default
# as it occasionally matters how Python e.g. reads files
env['LANG'] = "en_GB.UTF-8"
# Binaries made by packages installed by pip
env["PATH"] = join(self.hostpython_site_dir, "bin") + ":" + env["PATH"]
env["PATH"] = self._host_recipe.site_bin + ":" + env["PATH"]

host_env = self.get_hostrecipe_env()
env['PYTHONPATH'] = host_env["PYTHONPATH"]

if not self.call_hostpython_via_targetpython:
env['CFLAGS'] += ' -I{}'.format(
@@ -936,24 +968,11 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True):
self.ctx.python_recipe.link_root(arch.arch),
self.ctx.python_recipe.link_version,
)

hppath = []
hppath.append(join(dirname(self.hostpython_location), 'Lib'))
hppath.append(join(hppath[0], 'site-packages'))
builddir = join(dirname(self.hostpython_location), 'build')
if exists(builddir):
hppath += [join(builddir, d) for d in listdir(builddir)
if isdir(join(builddir, d))]
if len(hppath) > 0:
if 'PYTHONPATH' in env:
env['PYTHONPATH'] = ':'.join(hppath + [env['PYTHONPATH']])
else:
env['PYTHONPATH'] = ':'.join(hppath)
return env

def should_build(self, arch):
name = self.folder_name
if self.ctx.has_package(name, arch):
if self.ctx.has_package(name, arch) or name in listdir(self._host_recipe.site_dir):
info('Python package already exists in site-packages')
return False
info('{} apparently isn\'t already in site-packages'.format(name))
@@ -980,38 +999,39 @@ def install_python_package(self, arch, name=None, env=None, is_dir=True):
hostpython = sh.Command(self.hostpython_location)
hpenv = env.copy()
with current_directory(self.get_build_dir(arch.arch)):
shprint(hostpython, 'setup.py', 'install', '-O2',
'--root={}'.format(self.ctx.get_python_install_dir(arch.arch)),
'--install-lib=.',
_env=hpenv, *self.setup_extra_args)
if isfile("setup.py"):
shprint(hostpython, 'setup.py', 'install', '-O2',

# If asked, also install in the hostpython build dir
if self.install_in_hostpython:
self.install_hostpython_package(arch)
'--root={}'.format(self.ctx.get_python_install_dir(arch.arch)),
'--install-lib=.',
_env=hpenv, *self.setup_extra_args)

# If asked, also install in the hostpython build dir
if self.install_in_hostpython:
self.install_hostpython_package(arch)
else:
warning("`PythonRecipe.install_python_package` called without `setup.py` file!")

def get_hostrecipe_env(self, arch):
def get_hostrecipe_env(self):
env = environ.copy()
env['PYTHONPATH'] = self.hostpython_site_dir
_python_path = self._host_recipe.get_path_to_python()
env['PYTHONPATH'] = self._host_recipe.site_dir + ":" + join(
_python_path, "Modules") + ":" + glob.glob(join(_python_path, "build", "lib*"))[0]
return env

@property
def hostpython_site_dir(self):
return join(dirname(self.real_hostpython_location), 'Lib', 'site-packages')

def install_hostpython_package(self, arch):
env = self.get_hostrecipe_env(arch)
env = self.get_hostrecipe_env()
real_hostpython = sh.Command(self.real_hostpython_location)
shprint(real_hostpython, 'setup.py', 'install', '-O2',
'--root={}'.format(dirname(self.real_hostpython_location)),
'--install-lib=Lib/site-packages',
'--root={}'.format(self._host_recipe.site_root),
_env=env, *self.setup_extra_args)

@property
def python_major_minor_version(self):
parsed_version = packaging.version.parse(self.ctx.python_recipe.version)
return f"{parsed_version.major}.{parsed_version.minor}"

def install_hostpython_prerequisites(self, packages=None, force_upgrade=True):
def install_hostpython_prerequisites(self, packages=None, force_upgrade=True, arch=None):
if not packages:
packages = self.hostpython_prerequisites

@@ -1021,15 +1041,16 @@ def install_hostpython_prerequisites(self, packages=None, force_upgrade=True):
pip_options = [
"install",
*packages,
"--target", self.hostpython_site_dir, "--python-version",
"--target", self._host_recipe.site_dir, "--python-version",
self.ctx.python_recipe.version,
# Don't use sources, instead wheels
"--only-binary=:all:",
]
if force_upgrade:
pip_options.append("--upgrade")
# Use system's pip
shprint(sh.pip, *pip_options)
pip_env = self.get_hostrecipe_env()
pip_env["HOME"] = "?"
shprint(sh.Command(self.real_hostpython_location), "-m", "pip", *pip_options, _env=pip_env)

def restore_hostpython_prerequisites(self, packages):
_packages = []
@@ -1068,13 +1089,12 @@ def build_compiled_components(self, arch):
env['STRIP'], '{}', ';', _env=env)

def install_hostpython_package(self, arch):
env = self.get_hostrecipe_env(arch)
env = self.get_hostrecipe_env()
self.rebuild_compiled_components(arch, env)
super().install_hostpython_package(arch)

def rebuild_compiled_components(self, arch, env):
info('Rebuilding compiled components in {}'.format(self.name))

hostpython = sh.Command(self.real_hostpython_location)
shprint(hostpython, 'setup.py', 'clean', '--all', _env=env)
shprint(hostpython, 'setup.py', self.build_cmd, '-v', _env=env,
@@ -1108,7 +1128,7 @@ def build_cython_components(self, arch):

with current_directory(self.get_build_dir(arch.arch)):
hostpython = sh.Command(self.ctx.hostpython)
shprint(hostpython, '-c', 'import sys; print(sys.path)', _env=env)
shprint(hostpython, '-c', 'import sys; print(sys.path, sys.exec_prefix, sys.prefix)', _env=env)
debug('cwd is {}'.format(realpath(curdir)))
info('Trying first build of {} to get cython files: this is '
'expected to fail'.format(self.name))
@@ -1198,7 +1218,7 @@ def get_recipe_env(self, arch, with_flags_in_cc=True):


class PyProjectRecipe(PythonRecipe):
"""Recipe for projects which contain `pyproject.toml`"""
'''Recipe for projects which contain `pyproject.toml`'''

# Extra args to pass to `python -m build ...`
extra_build_args = []
@@ -1223,17 +1243,14 @@ def get_recipe_env(self, arch, **kwargs):
return env

def get_wheel_platform_tag(self, arch):
return "android_" + {
return f"android_{self.ctx.ndk_api}_" + {
"armeabi-v7a": "arm",
"arm64-v8a": "aarch64",
"x86_64": "x86_64",
"x86": "i686",
}[arch.arch]

def install_wheel(self, arch, built_wheels):
with patch_wheel_setuptools_logging():
from wheel.cli.tags import tags as wheel_tags
from wheel.wheelfile import WheelFile
_wheel = built_wheels[0]
built_wheel_dir = dirname(_wheel)
# Fix wheel platform tag
@@ -1244,17 +1261,19 @@ def install_wheel(self, arch, built_wheels):
)
selected_wheel = join(built_wheel_dir, wheel_tag)

_dev_wheel_dir = environ.get("P4A_WHEEL_DIR", False)
if _dev_wheel_dir:
ensure_dir(_dev_wheel_dir)
shprint(sh.cp, selected_wheel, _dev_wheel_dir)
if self.ctx.save_prebuilt:
shprint(sh.cp, selected_wheel, self.ctx.prebuilt_dir)

def _(arch, wheel_tag, selected_wheel):
info(f"Installing built wheel: {wheel_tag}")
destination = self.ctx.get_python_install_dir(arch.arch)
with WheelFile(selected_wheel) as wf:
for zinfo in wf.filelist:
wf.extract(zinfo, destination)
wf.close()

self.install_libraries = lambda arch, wheel_tag=wheel_tag, selected_wheel=selected_wheel : _(arch, wheel_tag, selected_wheel)

info(f"Installing built wheel: {wheel_tag}")
destination = self.ctx.get_python_install_dir(arch.arch)
with WheelFile(selected_wheel) as wf:
for zinfo in wf.filelist:
wf.extract(zinfo, destination)
wf.close()

def build_arch(self, arch):
self.install_hostpython_prerequisites(
@@ -1282,6 +1301,7 @@ def build_arch(self, arch):
sh.Command(self.ctx.python_recipe.python_exe), *build_args, _env=env
)
built_wheels = [realpath(whl) for whl in glob.glob("dist/*.whl")]

self.install_wheel(arch, built_wheels)


83 changes: 83 additions & 0 deletions pythonforandroid/recipebuild.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import sys
import os
from argparse import ArgumentParser

from pythonforandroid.recipe import Recipe
from pythonforandroid.logger import setup_color, info_main, Colo_Fore, info
from pythonforandroid.build import Context
from pythonforandroid.graph import get_recipe_order_and_bootstrap
from pythonforandroid.util import ensure_dir
from pythonforandroid.bootstraps.empty import bootstrap
from pythonforandroid.distribution import Distribution
from pythonforandroid.androidndk import AndroidNDK

DEFAULT_RECIPES = ["sdl2"]

class RecipeBuilder:
def __init__(self, parsed_args):
setup_color(True)
self.build_dir = parsed_args.workdir
self.init_context(parsed_args)
self.build_recipes(
set(parsed_args.recipes + DEFAULT_RECIPES),
set(parsed_args.arch)
)

def init_context(self, parse_args):
self.ctx = Context()
self.ctx.save_prebuilt = True
self.ctx.setup_dirs(self.build_dir)
self.ctx.ndk_api = parse_args.min_api
self.ctx.android_api = parse_args.target_api
self.ctx.ndk_dir = "/home/tdynamos/.buildozer/android/platform/android-ndk-r25b"

def build_recipes(self, recipes, archs):
info_main(f"# Requested recipes: {Colo_Fore.BLUE}{recipes}")

_recipes, _non_recipes, bootstrap = get_recipe_order_and_bootstrap(
self.ctx, recipes
)
self.ctx.prepare_bootstrap(bootstrap)
self.ctx.set_archs(archs)
self.ctx.bootstrap.distribution = Distribution.get_distribution(
self.ctx, name=bootstrap.name, recipes=recipes, archs=archs,
)
self.ctx.ndk = AndroidNDK("/home/tdynamos/.buildozer/android/platform/android-ndk-r25b")
recipes = [Recipe.get_recipe(recipe, self.ctx) for recipe in _recipes]

self.ctx.recipe_build_order = _recipes
for recipe in recipes:
recipe.download_if_necessary()

for arch in self.ctx.archs:
info_main("# Building all recipes for arch {}".format(arch.arch))

info_main("# Unpacking recipes")
for recipe in recipes:
ensure_dir(recipe.get_build_container_dir(arch.arch))
recipe.prepare_build_dir(arch.arch)

info_main("# Prebuilding recipes")
# 2) prebuild packages
for recipe in recipes:
info_main("Prebuilding {} for {}".format(recipe.name, arch.arch))
recipe.prebuild_arch(arch)
recipe.apply_patches(arch)

info_main("# Building recipes")
for recipe in recipes:
info_main("Building {} for {}".format(recipe.name, arch.arch))
if recipe.should_build(arch):
recipe.build_arch(arch)
else:
info("{} said it is already built, skipping".format(recipe.name))
recipe.install_libraries(arch)

if __name__ == "__main__":
parser = ArgumentParser(description="Build and package recipes.")
parser.add_argument('-r', '--recipes', nargs='+', help='Recipes to build.', required=True)
parser.add_argument('-a', '--arch', nargs='+', help='Android arch(s) to build.', required=True)
parser.add_argument('-w', '--workdir', type=str, help="Workdir for building recipes.", required=True)
parser.add_argument('-m', '--min-api', type=int, help="Android ndk (minimum) api.", default=24)
parser.add_argument('-t', '--target-api', type=int, help="Android target api.", default=24)
RecipeBuilder(parser.parse_args())
12 changes: 8 additions & 4 deletions pythonforandroid/recipes/android/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
from pythonforandroid.recipe import CythonRecipe, IncludedFilesBehaviour
from pythonforandroid.recipe import PyProjectRecipe, IncludedFilesBehaviour, Recipe
from pythonforandroid.util import current_directory
from pythonforandroid import logger

from os.path import join


class AndroidRecipe(IncludedFilesBehaviour, CythonRecipe):
class AndroidRecipe(IncludedFilesBehaviour, PyProjectRecipe):
# name = 'android'
version = None
url = None

src_filename = 'src'

depends = [('sdl2', 'genericndkbuild'), 'pyjnius']
hostpython_prerequisites = ["cython"]

config_env = {}

def get_recipe_env(self, arch):
env = super().get_recipe_env(arch)
def get_recipe_env(self, arch, **kwargs):
env = super().get_recipe_env(arch, **kwargs)
env.update(self.config_env)
return env

@@ -49,6 +50,9 @@ def prebuild_arch(self, arch):
'BOOTSTRAP': bootstrap,
'IS_SDL2': int(is_sdl2),
'PY2': 0,
'ANDROID_LIBS_DIR':join(
Recipe.get_recipe("sdl2", self.ctx).get_build_dir(arch.arch), "../..", "libs", arch.arch
),
'JAVA_NAMESPACE': java_ns,
'JNI_NAMESPACE': jni_ns,
'ACTIVITY_CLASS_NAME': self.ctx.activity_class_name,
34 changes: 24 additions & 10 deletions pythonforandroid/recipes/android/src/setup.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
from distutils.core import setup, Extension
from Cython.Build import cythonize
import os

library_dirs = ['libs/' + os.environ['ARCH']]
# Define the library directories
library_dirs = [os.environ['ANDROID_LIBS_DIR']]
lib_dict = {
'sdl2': ['SDL2', 'SDL2_image', 'SDL2_mixer', 'SDL2_ttf']
}
sdl_libs = lib_dict.get(os.environ['BOOTSTRAP'], ['main'])

modules = [Extension('android._android',
['android/_android.c', 'android/_android_jni.c'],
libraries=sdl_libs + ['log'],
library_dirs=library_dirs),
Extension('android._android_billing',
['android/_android_billing.c', 'android/_android_billing_jni.c'],
libraries=['log'],
library_dirs=library_dirs)]
# Define the extensions with Cython
modules = [
Extension('android._android',
['android/_android.pyx', 'android/_android_jni.c'],
libraries=sdl_libs + ['log'],
library_dirs=library_dirs),
Extension('android._android_billing',
['android/_android_billing.pyx', 'android/_android_billing_jni.c'],
libraries=['log'],
library_dirs=library_dirs),
Extension('android._android_sound',
['android/_android_sound.pyx', 'android/_android_sound_jni.c'],
libraries=['log'],
library_dirs=library_dirs)
]


# Use cythonize to build the modules
cythonized_modules = cythonize(modules, compiler_directives={'language_level': "3"})

# Setup the package
setup(name='android',
version='1.0',
packages=['android'],
package_dir={'android': 'android'},
ext_modules=modules
ext_modules=cythonized_modules
)
4 changes: 2 additions & 2 deletions pythonforandroid/recipes/genericndkbuild/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from os.path import join

from multiprocessing import cpu_count
from pythonforandroid.recipe import BootstrapNDKRecipe
from pythonforandroid.toolchain import current_directory, shprint
import sh
@@ -29,7 +29,7 @@ def build_arch(self, arch):
env = self.get_recipe_env(arch)

with current_directory(self.get_jni_dir()):
shprint(sh.Command(join(self.ctx.ndk_dir, "ndk-build")), "V=1", _env=env)
shprint(sh.Command(join(self.ctx.ndk_dir, "ndk-build")), "V=1", "-j", str(cpu_count()), _env=env)


recipe = GenericNDKBuildRecipe()
37 changes: 30 additions & 7 deletions pythonforandroid/recipes/hostpython3/__init__.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,8 @@
from pathlib import Path
from os.path import join

from pythonforandroid.logger import shprint
from packaging.version import Version
from pythonforandroid.logger import shprint, info
from pythonforandroid.recipe import Recipe
from pythonforandroid.util import (
BuildInterruptingException,
@@ -35,19 +36,17 @@ class HostPython3Recipe(Recipe):
:class:`~pythonforandroid.python.HostPythonRecipe`
'''

version = '3.11.5'
name = 'hostpython3'
version = '3.13.0'
_p_version = Version(version)
url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz'

build_subdir = 'native-build'
'''Specify the sub build directory for the hostpython3 recipe. Defaults
to ``native-build``.'''

url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz'
'''The default url to download our host python recipe. This url will
change depending on the python version set in attribute :attr:`version`.'''

patches = ['patches/pyconfig_detection.patch']

@property
def _exe_name(self):
'''
@@ -94,6 +93,26 @@ def get_build_dir(self, arch=None):

def get_path_to_python(self):
return join(self.get_build_dir(), self.build_subdir)

@property
def site_root(self):
return join(self.get_path_to_python(), "root")

@property
def site_bin(self):
dir = None
# TODO: implement mac os
if os.name == "posix":
dir = "usr/local/bin/"
return join(self.site_root, dir)

@property
def site_dir(self):
dir = None
# TODO: implement mac os
if os.name == "posix":
dir = f"usr/local/lib/python{self._p_version.major}.{self._p_version.minor}/site-packages/"
return join(self.site_root, dir)

def build_arch(self, arch):
env = self.get_recipe_env(arch)
@@ -138,7 +157,11 @@ def build_arch(self, arch):
shprint(sh.cp, exe, self.python_exe)
break

ensure_dir(self.site_root)
self.ctx.hostpython = self.python_exe

shprint(
sh.Command(self.python_exe), "-m", "ensurepip", "--root", self.site_root, "-U",
_env={"HOME":"?"} # need to hack HOME a bit
)

recipe = HostPython3Recipe()

This file was deleted.

63 changes: 11 additions & 52 deletions pythonforandroid/recipes/kivy/__init__.py
Original file line number Diff line number Diff line change
@@ -4,67 +4,26 @@
import packaging.version

import sh
from pythonforandroid.recipe import CythonRecipe
from pythonforandroid.recipe import PyProjectRecipe
from pythonforandroid.toolchain import current_directory, shprint


def is_kivy_affected_by_deadlock_issue(recipe=None, arch=None):
with current_directory(join(recipe.get_build_dir(arch.arch), "kivy")):
kivy_version = shprint(
sh.Command(sys.executable),
"-c",
"import _version; print(_version.__version__)",
)

return packaging.version.parse(
str(kivy_version)
) < packaging.version.Version("2.2.0.dev0")


class KivyRecipe(CythonRecipe):
version = '2.3.1'
class KivyRecipe(PyProjectRecipe):
version = 'master'
url = 'https://github.com/kivy/kivy/archive/{version}.zip'
name = 'kivy'

depends = ['sdl2', 'pyjnius', 'setuptools']
depends = ['sdl2', 'pyjnius']
python_depends = ['certifi', 'chardet', 'idna', 'requests', 'urllib3', 'filetype']

# sdl-gl-swapwindow-nogil.patch is needed to avoid a deadlock.
# See: https://github.com/kivy/kivy/pull/8025
# WARNING: Remove this patch when a new Kivy version is released.
patches = [("sdl-gl-swapwindow-nogil.patch", is_kivy_affected_by_deadlock_issue)]

def cythonize_build(self, env, build_dir='.'):
super().cythonize_build(env, build_dir=build_dir)

if not exists(join(build_dir, 'kivy', 'include')):
return

# If kivy is new enough to use the include dir, copy it
# manually to the right location as we bypass this stage of
# the build
with current_directory(build_dir):
build_libs_dirs = glob.glob(join('build', 'lib.*'))

for dirn in build_libs_dirs:
shprint(sh.cp, '-r', join('kivy', 'include'),
join(dirn, 'kivy'))

def cythonize_file(self, env, build_dir, filename):
# We can ignore a few files that aren't important to the
# android build, and may not work on Android anyway
do_not_cythonize = ['window_x11.pyx', ]
if basename(filename) in do_not_cythonize:
return
super().cythonize_file(env, build_dir, filename)

def get_recipe_env(self, arch):
env = super().get_recipe_env(arch)
patches = ["use_cython.patch"]

def get_recipe_env(self, arch, **kwargs):
env = super().get_recipe_env(arch, **kwargs)
# NDKPLATFORM is our switch for detecting Android platform, so can't be None
env['NDKPLATFORM'] = "NOTNONE"
env['LIBLINK'] = "NOTNONE"
if 'sdl2' in self.ctx.recipe_build_order:
env['USE_SDL2'] = '1'
env['KIVY_SPLIT_EXAMPLES'] = '1'
sdl_recipe = self.get_recipe("sdl2", self.ctx)
sdl2_mixer_recipe = self.get_recipe('sdl2_mixer', self.ctx)
sdl2_image_recipe = self.get_recipe('sdl2_image', self.ctx)
env['KIVY_SDL2_PATH'] = ':'.join([
@@ -73,7 +32,7 @@ def get_recipe_env(self, arch):
*sdl2_mixer_recipe.get_include_dirs(arch),
join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_ttf'),
])

env["LDFLAGS"] += " -L" + join(sdl_recipe.get_build_dir(arch.arch), "../..", "libs", arch.arch)
return env


32 changes: 0 additions & 32 deletions pythonforandroid/recipes/kivy/sdl-gl-swapwindow-nogil.patch

This file was deleted.

11 changes: 11 additions & 0 deletions pythonforandroid/recipes/kivy/use_cython.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- kivy-master/setup.py 2025-02-25 03:08:18.000000000 +0530
+++ kivy-master.mod/setup.py 2025-03-01 13:10:24.227808612 +0530
@@ -249,7 +249,7 @@
# This determines whether Cython specific functionality may be used.
can_use_cython = True

-if platform in ('ios', 'android'):
+if platform in ('ios'):
# NEVER use or declare cython on these platforms
print('Not using cython on %s' % platform)
can_use_cython = False
38 changes: 38 additions & 0 deletions pythonforandroid/recipes/libb2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from pythonforandroid.recipe import Recipe
from pythonforandroid.toolchain import current_directory, shprint, warning
from os.path import join
from multiprocessing import cpu_count
import sh


class Libb2Recipe(Recipe):
version = '0.98.1'
url = 'https://github.com/BLAKE2/libb2/releases/download/v{version}/libb2-{version}.tar.gz'
built_libraries = {'libb2.so': './src/.libs/', "libomp.so": "./src/.libs"}

def build_arch(self, arch):
# TODO: this build fails for x86_64 and x86
# checking whether mmx is supported... /home/tdynamos/p4acache/build/other_builds/libb2/x86_64__ndk_target_24/libb2/configure: line 13165: 0xunknown: value too great for base (error token is "0xunknown")
if arch.arch in ["x86_64", "x86"]:
warning(f"libb2 build disabled for {arch.arch}")
self.built_libraries = {}
return

with current_directory(self.get_build_dir(arch.arch)):
env = self.get_recipe_env(arch)
flags = [
'--host=' + arch.command_prefix,
]
configure = sh.Command('./configure')
shprint(configure, *flags, _env=env)
shprint(sh.make, "-j", str(cpu_count()), _env=env)
arch_ = {"armeabi-v7a":"arm", "arm64-v8a": "aarch64", "x86_64":"x86_64", "x86":"i386"}[arch.arch]
# also requires libomp.so
shprint(
sh.cp,
join(self.ctx.ndk.llvm_prebuilt_dir, f"lib64/clang/14.0.6/lib/linux/{arch_}/libomp.so"),
"./src/.libs"
)


recipe = Libb2Recipe()
8 changes: 6 additions & 2 deletions pythonforandroid/recipes/libcurl/__init__.py
Original file line number Diff line number Diff line change
@@ -7,11 +7,15 @@


class LibcurlRecipe(Recipe):
version = '7.55.1'
url = 'https://curl.haxx.se/download/curl-7.55.1.tar.gz'
version = '8.8.0'
url = 'https://github.com/curl/curl/releases/download/curl-{_version}/curl-{version}.tar.gz'
built_libraries = {'libcurl.so': 'dist/lib'}
depends = ['openssl']

@property
def versioned_url(self):
return self.url.format(version=self.version, _version=self.version.replace(".", "_"))

def build_arch(self, arch):
env = self.get_recipe_env(arch)

4 changes: 2 additions & 2 deletions pythonforandroid/recipes/libffi/__init__.py
Original file line number Diff line number Diff line change
@@ -14,8 +14,8 @@ class LibffiRecipe(Recipe):
- `libltdl-dev` which defines the `LT_SYS_SYMBOL_USCORE` macro
"""
name = 'libffi'
version = 'v3.4.2'
url = 'https://github.com/libffi/libffi/archive/{version}.tar.gz'
version = '3.4.6'
url = 'https://github.com/libffi/libffi/archive/v{version}.tar.gz'

patches = ['remove-version-info.patch']

2 changes: 1 addition & 1 deletion pythonforandroid/recipes/liblzma/__init__.py
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@

class LibLzmaRecipe(Recipe):

version = '5.2.4'
version = '5.6.2'
url = 'https://tukaani.org/xz/xz-{version}.tar.gz'
built_libraries = {'liblzma.so': 'p4a_install/lib'}

11 changes: 1 addition & 10 deletions pythonforandroid/recipes/libsodium/__init__.py
Original file line number Diff line number Diff line change
@@ -3,24 +3,15 @@
from pythonforandroid.logger import shprint
from multiprocessing import cpu_count
import sh
from packaging import version as packaging_version


class LibsodiumRecipe(Recipe):
version = '1.0.16'
url = 'https://github.com/jedisct1/libsodium/releases/download/{}/libsodium-{}.tar.gz'
url = 'https://github.com/jedisct1/libsodium/releases/download/{version}/libsodium-{version}.tar.gz'
depends = []
patches = ['size_max_fix.patch']
built_libraries = {'libsodium.so': 'src/libsodium/.libs'}

@property
def versioned_url(self):
asked_version = packaging_version.parse(self.version)
if asked_version > packaging_version.parse('1.0.16'):
return self._url.format(self.version + '-RELEASE', self.version)
else:
return self._url.format(self.version, self.version)

def build_arch(self, arch):
env = self.get_recipe_env(arch)
with current_directory(self.get_build_dir(arch.arch)):
39 changes: 15 additions & 24 deletions pythonforandroid/recipes/openssl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from os.path import join
from multiprocessing import cpu_count

from pythonforandroid.recipe import Recipe
from pythonforandroid.util import current_directory
@@ -44,35 +45,24 @@ class OpenSSLRecipe(Recipe):
'''

version = '1.1'
'''the major minor version used to link our recipes'''

url_version = '1.1.1w'
'''the version used to download our libraries'''

url = 'https://www.openssl.org/source/openssl-{url_version}.tar.gz'
version = '3.3.1'
url = 'https://www.openssl.org/source/openssl-{version}.tar.gz'

built_libraries = {
'libcrypto{version}.so'.format(version=version): '.',
'libssl{version}.so'.format(version=version): '.',
'libcrypto.so': '.',
'libssl.so': '.',
}

@property
def versioned_url(self):
if self.url is None:
return None
return self.url.format(url_version=self.url_version)

def get_build_dir(self, arch):
return join(
self.get_build_container_dir(arch), self.name + self.version
self.get_build_container_dir(arch), self.name + self.version[0]
)

def include_flags(self, arch):
'''Returns a string with the include folders'''
openssl_includes = join(self.get_build_dir(arch.arch), 'include')
return (' -I' + openssl_includes +
' -I' + join(openssl_includes, 'internal') +
# ' -I' + join(openssl_includes, 'internal') +
' -I' + join(openssl_includes, 'openssl'))

def link_dirs_flags(self, arch):
@@ -85,7 +75,7 @@ def link_libs_flags(self):
'''Returns a string with the appropriate `-l<lib>` flags to link with
the openssl libs. This string is usually added to the environment
variable `LIBS`'''
return ' -lcrypto{version} -lssl{version}'.format(version=self.version)
return ' -lcrypto -lssl'

def link_flags(self, arch):
'''Returns a string with the flags to link with the openssl libraries
@@ -94,10 +84,12 @@ def link_flags(self, arch):

def get_recipe_env(self, arch=None):
env = super().get_recipe_env(arch)
env['OPENSSL_VERSION'] = self.version
env['MAKE'] = 'make' # This removes the '-j5', which isn't safe
env['OPENSSL_VERSION'] = self.version[0]
env['CC'] = 'clang'
env['ANDROID_NDK_HOME'] = self.ctx.ndk_dir
env['ANDROID_NDK_ROOT'] = self.ctx.ndk_dir
env["PATH"] = f"{self.ctx.ndk.llvm_bin_dir}:{env['PATH']}"
env["CFLAGS"] += " -Wno-macro-redefined"
env["MAKE"]= "make"
return env

def select_build_arch(self, arch):
@@ -125,13 +117,12 @@ def build_arch(self, arch):
'shared',
'no-dso',
'no-asm',
'no-tests',
buildarch,
'-D__ANDROID_API__={}'.format(self.ctx.ndk_api),
]
shprint(perl, 'Configure', *config_args, _env=env)
self.apply_patch('disable-sover.patch', arch.arch)

shprint(sh.make, 'build_libs', _env=env)
shprint(sh.make, '-j', str(cpu_count()), _env=env)


recipe = OpenSSLRecipe()
17 changes: 10 additions & 7 deletions pythonforandroid/recipes/pyjnius/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
from pythonforandroid.recipe import CythonRecipe
from pythonforandroid.recipe import PyProjectRecipe
from pythonforandroid.toolchain import shprint, current_directory, info
from pythonforandroid.patching import will_build
import sh
from os.path import join


class PyjniusRecipe(CythonRecipe):
class PyjniusRecipe(PyProjectRecipe):
version = '1.6.1'
url = 'https://github.com/kivy/pyjnius/archive/{version}.zip'
name = 'pyjnius'
depends = [('genericndkbuild', 'sdl2'), 'six']
depends = ['six']
site_packages_name = 'jnius'
patches = [('genericndkbuild_jnienv_getter.patch', will_build('genericndkbuild')), "use_cython.patch"]

patches = [('genericndkbuild_jnienv_getter.patch', will_build('genericndkbuild'))]

def get_recipe_env(self, arch):
env = super().get_recipe_env(arch)
def get_recipe_env(self, arch, **kwargs):
env = super().get_recipe_env(arch, **kwargs)
# NDKPLATFORM is our switch for detecting Android platform, so can't be None
env['NDKPLATFORM'] = "NOTNONE"
env['LIBLINK'] = "NOTNONE"
env["ANDROID_PYJNIUS_CYTHON_3"] = "1"
sdl_recipe = self.get_recipe("sdl2", self.ctx)
env["LDFLAGS"] += " -L" + join(sdl_recipe.get_build_dir(arch.arch), "../..", "libs", arch.arch)
return env

def postbuild_arch(self, arch):
13 changes: 13 additions & 0 deletions pythonforandroid/recipes/pyjnius/use_cython.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--- pyjnius-1.6.1/setup.py 2023-11-05 21:07:43.000000000 +0530
+++ pyjnius-1.6.1.mod/setup.py 2025-03-01 14:47:11.964847337 +0530
@@ -59,10 +59,6 @@
if NDKPLATFORM is not None and getenv('LIBLINK'):
PLATFORM = 'android'

-# detect platform
-if PLATFORM == 'android':
- PYX_FILES = [fn[:-3] + 'c' for fn in PYX_FILES]
-
JAVA=get_java_setup(PLATFORM)

assert JAVA.is_jdk(), "You need a JDK, we only found a JRE. Try setting JAVA_HOME"
196 changes: 111 additions & 85 deletions pythonforandroid/recipes/python3/__init__.py
Original file line number Diff line number Diff line change
@@ -5,8 +5,10 @@
from os import environ, utime
from os.path import dirname, exists, join
from pathlib import Path
from multiprocessing import cpu_count
import shutil

from packaging.version import Version
from pythonforandroid.logger import info, warning, shprint
from pythonforandroid.patching import version_starts_with
from pythonforandroid.recipe import Recipe, TargetPythonRecipe
@@ -40,73 +42,75 @@ class Python3Recipe(TargetPythonRecipe):
- _ctypes: you must add the recipe for ``libffi``.
- _sqlite3: you must add the recipe for ``sqlite3``.
- _ssl: you must add the recipe for ``openssl``.
- _bz2: you must add the recipe for ``libbz2`` (optional).
- _lzma: you must add the recipe for ``liblzma`` (optional).
- _bz2: you must add the recipe for ``libbz2``.
- _lzma: you must add the recipe for ``liblzma``.
.. note:: This recipe can be built only against API 21+.
.. versionchanged:: 2019.10.06.post0
- Refactored from deleted class ``python.GuestPythonRecipe`` into here
- Added optional dependencies: :mod:`~pythonforandroid.recipes.libbz2`
and :mod:`~pythonforandroid.recipes.liblzma`
.. versionchanged:: 0.6.0
Refactored into class
:class:`~pythonforandroid.python.GuestPythonRecipe`
'''

version = '3.11.5'
url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz'
version = '3.13.0'
_p_version = Version(version)
url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz'
name = 'python3'

patches = [
'patches/pyconfig_detection.patch',
'patches/reproducible-buildinfo.diff',
# Python 3.7.x
*(['patches/py3.7.1_fix-ctypes-util-find-library.patch',
'patches/py3.7.1_fix-zlib-version.patch',
'patches/py3.7.1_fix_cortex_a8.patch'
] if version.startswith("3.7") else []),

# Python < 3.11 patches
*([
'patches/py3.8.1.patch',
'patches/py3.8.1_fix_cortex_a8.patch'
] if _p_version < Version("3.11") else []),
]

# Python 3.7.1
('patches/py3.7.1_fix-ctypes-util-find-library.patch', version_starts_with("3.7")),
('patches/py3.7.1_fix-zlib-version.patch', version_starts_with("3.7")),
if _p_version >= Version("3.11") and _p_version < Version("3.13"):
# for 3.12 and 3.11
patches.append("patches/cpython-311-ctypes-find-library.patch")

# Python 3.8.1 & 3.9.X
('patches/py3.8.1.patch', version_starts_with("3.8")),
('patches/py3.8.1.patch', version_starts_with("3.9")),
('patches/py3.8.1.patch', version_starts_with("3.10")),
('patches/cpython-311-ctypes-find-library.patch', version_starts_with("3.11")),
]
if version_starts_with("3.13"):
# for 3.13
patches.append("patches/cpython-313-ctypes-find-library.patch")

if shutil.which('lld') is not None:
patches += [
("patches/py3.7.1_fix_cortex_a8.patch", version_starts_with("3.7")),
("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.8")),
("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.9")),
("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.10")),
("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.11")),
]

depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi']
# those optional depends allow us to build python compression modules:
# - _bz2.so
# - _lzma.so
opt_depends = ['libbz2', 'liblzma']
'''The optional libraries which we would like to get our python linked'''

configure_args = (
depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi', 'libbz2', 'libb2', 'liblzma', 'util-linux']

configure_args = [
'--host={android_host}',
'--build={android_build}',
'--enable-shared',
'--enable-ipv6',
'ac_cv_file__dev_ptmx=yes',
'ac_cv_file__dev_ptc=no',
'--enable-loadable-sqlite-extensions',
'--without-ensurepip',
'ac_cv_little_endian_double=yes',
'ac_cv_header_sys_eventfd_h=no',
'--without-static-libpython',
'--without-readline',

# Android prefix
'--prefix={prefix}',
'--exec-prefix={exec_prefix}',
'--enable-loadable-sqlite-extensions'
)

# Special cross compile args
'ac_cv_file__dev_ptmx=yes',
'ac_cv_file__dev_ptc=no',
'ac_cv_header_sys_eventfd_h=no',
'ac_cv_little_endian_double=yes',
]

if version_starts_with("3.11"):
configure_args += ('--with-build-python={python_host_bin}',)
if _p_version >= Version("3.11"):
configure_args.extend([
'--with-build-python={python_host_bin}',
])

'''The configure arguments needed to build the python recipe. Those are
used in method :meth:`build_arch` (if not overwritten like python3's
@@ -160,6 +164,9 @@ class Python3Recipe(TargetPythonRecipe):
longer used and has been removed in favour of extension .pyc
'''

disable_gil = False
'''python3.13 experimental free-threading build'''

def __init__(self, *args, **kwargs):
self._ctx = None
super().__init__(*args, **kwargs)
@@ -217,13 +224,10 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True):
)

env['LDFLAGS'] = env.get('LDFLAGS', '')
if shutil.which('lld') is not None:
# Note: The -L. is to fix a bug in python 3.7.
# https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234409
env['LDFLAGS'] += ' -L. -fuse-ld=lld'
else:
warning('lld not found, linking without it. '
'Consider installing lld if linker errors occur.')
# Note: The -L. is to fix a bug in python 3.7.
# https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234409
env["PATH"] = f"{self.ctx.ndk.llvm_bin_dir}:{env['PATH']}" # find lld
env['LDFLAGS'] += ' -L. -fuse-ld=lld'

return env

@@ -235,39 +239,53 @@ def add_flags(include_flags, link_dirs, link_libs):
env['CPPFLAGS'] = env.get('CPPFLAGS', '') + include_flags
env['LDFLAGS'] = env.get('LDFLAGS', '') + link_dirs
env['LIBS'] = env.get('LIBS', '') + link_libs

if 'sqlite3' in self.ctx.recipe_build_order:
info('Activating flags for sqlite3')
recipe = Recipe.get_recipe('sqlite3', self.ctx)
add_flags(' -I' + recipe.get_build_dir(arch.arch),
' -L' + recipe.get_lib_dir(arch), ' -lsqlite3')

if 'libffi' in self.ctx.recipe_build_order:
info('Activating flags for libffi')
recipe = Recipe.get_recipe('libffi', self.ctx)
# In order to force the correct linkage for our libffi library, we
# set the following variable to point where is our libffi.pc file,
# because the python build system uses pkg-config to configure it.
env['PKG_CONFIG_PATH'] = recipe.get_build_dir(arch.arch)
add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)),
' -L' + join(recipe.get_build_dir(arch.arch), '.libs'),
' -lffi')

if 'openssl' in self.ctx.recipe_build_order:
info('Activating flags for openssl')
recipe = Recipe.get_recipe('openssl', self.ctx)
self.configure_args += \
('--with-openssl=' + recipe.get_build_dir(arch.arch),)
add_flags(recipe.include_flags(arch),
recipe.link_dirs_flags(arch), recipe.link_libs_flags())

if self._p_version >= Version("3.11"):
info('Activating flags for libuuid')
add_flags(
' -I' + join(Recipe.get_recipe('util-linux', self.ctx).get_build_dir(arch.arch), "libuuid", "src"),
' -L' + self.ctx.get_libs_dir(arch.arch),
' -luuid'
)

# blake2
if arch.arch not in ["x86_64", "x86"]:
info('Activating flags for libb2')
add_flags(
' -I' + join(Recipe.get_recipe('libb2', self.ctx).get_build_dir(arch.arch), "src"),
'',
' -lb2'
)


info('Activating flags for sqlite3')
recipe = Recipe.get_recipe('sqlite3', self.ctx)
add_flags(' -I' + recipe.get_build_dir(arch.arch),
' -L' + recipe.get_lib_dir(arch), ' -lsqlite3')

info('Activating flags for libffi')
recipe = Recipe.get_recipe('libffi', self.ctx)
# In order to force the correct linkage for our libffi library, we
# set the following variable to point where is our libffi.pc file,
# because the python build system uses pkg-config to configure it.
env['PKG_CONFIG_PATH'] = recipe.get_build_dir(arch.arch)
add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)),
' -L' + join(recipe.get_build_dir(arch.arch), '.libs'),
' -lffi')

info('Activating flags for openssl')
recipe = Recipe.get_recipe('openssl', self.ctx)
self.configure_args += \
('--with-openssl=' + recipe.get_build_dir(arch.arch),)
add_flags(recipe.include_flags(arch),
recipe.link_dirs_flags(arch), recipe.link_libs_flags())

for library_name in {'libbz2', 'liblzma'}:
if library_name in self.ctx.recipe_build_order:
info(f'Activating flags for {library_name}')
recipe = Recipe.get_recipe(library_name, self.ctx)
add_flags(recipe.get_library_includes(arch),
recipe.get_library_ldflags(arch),
recipe.get_library_libs_flag())
info(f'Activating flags for {library_name}')
recipe = Recipe.get_recipe(library_name, self.ctx)
add_flags(recipe.get_library_includes(arch),
recipe.get_library_ldflags(arch),
recipe.get_library_libs_flag())

# python build system contains hardcoded zlib version which prevents
# the build of zlib module, here we search for android's zlib version
@@ -294,7 +312,9 @@ def add_flags(include_flags, link_dirs, link_libs):
)
env['ZLIB_VERSION'] = line.replace('#define ZLIB_VERSION ', '')
add_flags(' -I' + zlib_includes, ' -L' + zlib_lib_path, ' -lz')


if self._p_version >= Version("3.13") and self.disable_gil:
self.configure_args.append("--disable-gil")
return env

def build_arch(self, arch):
@@ -312,15 +332,20 @@ def build_arch(self, arch):
ensure_dir(build_dir)

# TODO: Get these dynamically, like bpo-30386 does
sys_prefix = '/usr/local'
sys_exec_prefix = '/usr/local'
sys_prefix = "/usr/local/"
sys_exec_prefix = "/usr/local/"

env = self.get_recipe_env(arch)
env = self.set_libs_flags(env, arch)

android_build = sh.Command(
join(recipe_build_dir,
'config.guess'))().strip()

# disable blake2 for x86 and x86_64
if arch.arch in ["x86_64", "x86"]:
warning(f"blake2 disabled for {arch.arch}")
self.configure_args.append("--with-builtin-hashlib-hashes=md5,sha1,sha2,sha3")

with current_directory(build_dir):
if not exists('config.status'):
@@ -336,11 +361,10 @@ def build_arch(self, arch):
exec_prefix=sys_exec_prefix)).split(' '),
_env=env)

# Python build does not seem to play well with make -j option from Python 3.11 and onwards
# Before losing some time, please check issue
# https://github.com/python/cpython/issues/101295 , as the root cause looks similar
shprint(
sh.make,
'-j',
str(cpu_count()),
'all',
'INSTSONAME={lib_name}'.format(lib_name=self._libpython),
_env=env
@@ -374,7 +398,9 @@ def create_python_bundle(self, dirn, arch):
self.get_build_dir(arch.arch),
'android-build',
'build',
'lib.linux{}-{}-{}'.format(
'lib.{}{}-{}-{}'.format(
# android is now supported platform
"android" if self._p_version >= Version("3.13") else "linux",
'2' if self.version[0] == '2' else '',
arch.command_prefix.split('-')[0],
self.major_minor_version_string
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- Python-3.13.0/Lib/ctypes/util.py 2024-10-07 10:32:14.000000000 +0530
+++ Python-3.11.5.mod/Lib/ctypes/util.py 2024-11-01 12:15:54.130409172 +0530
@@ -97,6 +97,8 @@

fname = f"{directory}/lib{name}.so"
return fname if os.path.isfile(fname) else None
+ from android._ctypes_library_finder import find_library as _find_lib
+ find_library = _find_lib

elif os.name == "posix":
# Andreas Degert's find functions, using gcc, /sbin/ldconfig, objdump
5 changes: 4 additions & 1 deletion pythonforandroid/recipes/sdl2/__init__.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

from pythonforandroid.recipe import BootstrapNDKRecipe
from pythonforandroid.toolchain import current_directory, shprint
from multiprocessing import cpu_count
import sh


@@ -12,7 +13,7 @@ class LibSDL2Recipe(BootstrapNDKRecipe):

dir_name = 'SDL'

depends = ['sdl2_image', 'sdl2_mixer', 'sdl2_ttf']
depends = ['sdl2_image', 'sdl2_mixer', 'sdl2_ttf', 'python3']

def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=True):
env = super().get_recipe_env(
@@ -32,6 +33,8 @@ def build_arch(self, arch):
shprint(
sh.Command(join(self.ctx.ndk_dir, "ndk-build")),
"V=1",
"-j",
str(cpu_count()),
"NDK_DEBUG=" + ("1" if self.ctx.build_as_debuggable else "0"),
_env=env
)
27 changes: 22 additions & 5 deletions pythonforandroid/recipes/sdl2_image/__init__.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@


class LibSDL2Image(BootstrapNDKRecipe):
version = '2.8.0'
version = '2.8.2'
url = 'https://github.com/libsdl-org/SDL_image/releases/download/release-{version}/SDL2_image-{version}.tar.gz'
dir_name = 'SDL2_image'

@@ -20,10 +20,27 @@ def get_include_dirs(self, arch):
def prebuild_arch(self, arch):
# We do not have a folder for each arch on BootstrapNDKRecipe, so we
# need to skip the external deps download if we already have done it.
external_deps_dir = os.path.join(self.get_build_dir(arch.arch), "external")
if not os.path.exists(os.path.join(external_deps_dir, "libwebp")):
with current_directory(external_deps_dir):
shprint(sh.Command("./download.sh"))

build_dir = self.get_build_dir(arch.arch)

with open(os.path.join(build_dir, ".gitmodules"), "r") as file:
for section in file.read().split('[submodule "')[1:]:
line_split = section.split(" = ")
# Parse .gitmoulde section
clone_path, url, branch = (
os.path.join(build_dir, line_split[1].split("\n")[0].strip()),
line_split[2].split("\n")[0].strip(),
line_split[-1].strip()
)
# Clone if needed
if not os.path.exists(clone_path) or os.listdir(clone_path) == 0:
shprint(
sh.git, "clone", url,
"--depth", "1", "-b",
branch, clone_path, "--recursive"
)
file.close()

super().prebuild_arch(arch)


3 changes: 2 additions & 1 deletion pythonforandroid/recipes/setuptools/__init__.py
Original file line number Diff line number Diff line change
@@ -3,9 +3,10 @@

class SetuptoolsRecipe(PythonRecipe):
version = '69.2.0'
url = 'https://pypi.python.org/packages/source/s/setuptools/setuptools-{version}.tar.gz'
url = ''
call_hostpython_via_targetpython = False
install_in_hostpython = True
hostpython_prerequisites = [f"setuptools=={version}"]


recipe = SetuptoolsRecipe()
10 changes: 0 additions & 10 deletions pythonforandroid/recipes/six/__init__.py

This file was deleted.

6 changes: 4 additions & 2 deletions pythonforandroid/recipes/sqlite3/__init__.py
Original file line number Diff line number Diff line change
@@ -6,12 +6,14 @@


class Sqlite3Recipe(NDKRecipe):
version = '3.35.5'
version = '3.46.0'
# Don't forget to change the URL when changing the version
url = 'https://www.sqlite.org/2021/sqlite-amalgamation-3350500.zip'
url = 'https://sqlite.org/2024/sqlite-amalgamation-3460000.zip'
generated_libraries = ['sqlite3']
built_libraries = {"libsqlite3.so": "."}

def should_build(self, arch):
self.built_libraries["libsqlite3.so"] = join(self.get_build_dir(arch.arch), 'libs', arch.arch)
return not self.has_libs(arch, 'libsqlite3.so')

def prebuild_arch(self, arch):
36 changes: 36 additions & 0 deletions pythonforandroid/recipes/util-linux/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pythonforandroid.recipe import Recipe
from pythonforandroid.toolchain import current_directory, shprint
from multiprocessing import cpu_count
import sh


class UTIL_LINUXRecipe(Recipe):
version = '2.40.2'
url = 'https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/v2.40/util-linux-{version}.tar.xz'
depends = ["libpthread"]
built_libraries = {'libuuid.so': './.libs/'}
utils = ["uuidd"] # enable more utils as per requirements

def get_recipe_env(self, arch, **kwargs):
env = super().get_recipe_env(arch, **kwargs)
if arch.arch in ["x86_64", "arm64_v8a"]:
env["ac_cv_func_prlimit"] = "yes"
return env

def build_arch(self, arch):
with current_directory(self.get_build_dir(arch.arch)):
env = self.get_recipe_env(arch)
flags = [
'--host=' + arch.command_prefix,
'--without-systemd',
]

if arch.arch in ["armeabi-v7a", "x86"]:
# Fun fact: Android 32 bit devices won't work in year 2038
flags.append("--disable-year2038")

configure = sh.Command('./configure')
shprint(configure, *flags, _env=env)
shprint(sh.make, "-j", str(cpu_count()), *self.utils, _env=env)

recipe = UTIL_LINUXRecipe()
31 changes: 21 additions & 10 deletions pythonforandroid/toolchain.py
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@

from pythonforandroid import __version__
from pythonforandroid.bootstrap import Bootstrap
from pythonforandroid.build import Context, build_recipes, project_has_setup_py
from pythonforandroid.build import Context, build_recipes
from pythonforandroid.distribution import Distribution, pretty_log_dists
from pythonforandroid.entrypoints import main
from pythonforandroid.graph import get_recipe_order_and_bootstrap
@@ -569,18 +569,18 @@ def add_parser(subparsers, *args, **kwargs):
args, unknown = parser.parse_known_args(sys.argv[1:])
args.unknown_args = unknown

if getattr(args, "private", None) is not None:
if hasattr(args, "private") and args.private is not None:
# Pass this value on to the internal bootstrap build.py:
args.unknown_args += ["--private", args.private]
if getattr(args, "build_mode", None) == "release":
if hasattr(args, "build_mode") and args.build_mode == "release":
args.unknown_args += ["--release"]
if getattr(args, "with_debug_symbols", False):
if hasattr(args, "with_debug_symbols") and args.with_debug_symbols:
args.unknown_args += ["--with-debug-symbols"]
if getattr(args, "ignore_setup_py", False):
if hasattr(args, "ignore_setup_py") and args.ignore_setup_py:
args.use_setup_py = False
if getattr(args, "activity_class_name", "org.kivy.android.PythonActivity") != 'org.kivy.android.PythonActivity':
if hasattr(args, "activity_class_name") and args.activity_class_name != 'org.kivy.android.PythonActivity':
args.unknown_args += ["--activity-class-name", args.activity_class_name]
if getattr(args, "service_class_name", "org.kivy.android.PythonService") != 'org.kivy.android.PythonService':
if hasattr(args, "service_class_name") and args.service_class_name != 'org.kivy.android.PythonService':
args.unknown_args += ["--service-class-name", args.service_class_name]

self.args = args
@@ -603,13 +603,21 @@ def add_parser(subparsers, *args, **kwargs):
args, "with_debug_symbols", False
)

have_setup_py_or_similar = False
if getattr(args, "private", None) is not None:
project_dir = getattr(args, "private")
if (os.path.exists(os.path.join(project_dir, "setup.py")) or
os.path.exists(os.path.join(project_dir,
"pyproject.toml"))):
have_setup_py_or_similar = True

# Process requirements and put version in environ
if hasattr(args, 'requirements'):
requirements = []

# Add dependencies from setup.py, but only if they are recipes
# (because otherwise, setup.py itself will install them later)
if (project_has_setup_py(getattr(args, "private", None)) and
if (have_setup_py_or_similar and
getattr(args, "use_setup_py", False)):
try:
info("Analyzing package dependencies. MAY TAKE A WHILE.")
@@ -690,7 +698,10 @@ def warn_on_deprecated_args(self, args):

# Output warning if setup.py is present and neither --ignore-setup-py
# nor --use-setup-py was specified.
if project_has_setup_py(getattr(args, "private", None)):
if getattr(args, "private", None) is not None and \
(os.path.exists(os.path.join(args.private, "setup.py")) or
os.path.exists(os.path.join(args.private, "pyproject.toml"))
):
if not getattr(args, "use_setup_py", False) and \
not getattr(args, "ignore_setup_py", False):
warning(" **** FUTURE BEHAVIOR CHANGE WARNING ****")
@@ -1024,7 +1035,7 @@ def _build_package(self, args, package_type):
# .../build/bootstrap_builds/sdl2-python3/gradlew
# if docker on windows, gradle contains CRLF
output = shprint(
sh.Command('dos2unix'), gradlew._path,
sh.Command('dos2unix'), gradlew._path.decode(encoding="utf-8"),
_tail=20, _critical=True, _env=env
)
if args.build_mode == "debug":