From 6a133842db59000616c2fe54a84ecf396aa3c428 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 28 Oct 2025 02:22:25 +0100 Subject: [PATCH] Updated build recipes --- recipes/able_recipe/__init__.py | 37 + recipes/able_recipe/setup.py | 9 + recipes/codec2/__init__.py | 1 + recipes/cython/__init__.py | 14 + recipes/ffpyplayer/__init__.py | 1480 +---------------- recipes/ffpyplayer/setup.py.patch | 4 +- recipes/hostpython3/__init__.py | 175 ++ .../patches/pyconfig_detection.patch | 13 + recipes/jpeg/Application.mk | 4 + recipes/jpeg/__init__.py | 58 + recipes/jpeg/build-static.patch | 85 + recipes/jpeg/remove-version.patch | 12 + recipes/numpy/__init__.py | 75 - .../add_libm_explicitly_to_build.patch | 20 - recipes/numpy/patches/ranlib.patch | 11 - .../numpy/patches/remove-default-paths.patch | 28 - recipes/pycodec2/__init__.py | 2 +- recipes/pyjnius/__init__.py | 44 + .../genericndkbuild_jnienv_getter.patch | 24 + recipes/pyjnius/sdl3_jnienv_getter.patch | 24 + recipes/pyjnius/use_cython.patch | 13 + recipes/python3/__init__.py | 445 +++++ .../cpython-311-ctypes-find-library.patch | 19 + ...py3.7.1_fix-ctypes-util-find-library.patch | 15 + .../patches/py3.7.1_fix-zlib-version.patch | 12 + .../patches/py3.7.1_fix_cortex_a8.patch | 14 + recipes/python3/patches/py3.8.1.patch | 42 + .../patches/py3.8.1_fix_cortex_a8.patch | 15 + .../python3/patches/pyconfig_detection.patch | 13 + .../patches/reproducible-buildinfo.diff | 13 + recipes/sqlite3/Android.mk | 11 + recipes/sqlite3/__init__.py | 36 + 32 files changed, 1154 insertions(+), 1614 deletions(-) create mode 100644 recipes/able_recipe/__init__.py create mode 100644 recipes/able_recipe/setup.py create mode 100644 recipes/cython/__init__.py create mode 100644 recipes/hostpython3/__init__.py create mode 100644 recipes/hostpython3/patches/pyconfig_detection.patch create mode 100644 recipes/jpeg/Application.mk create mode 100644 recipes/jpeg/__init__.py create mode 100644 recipes/jpeg/build-static.patch create mode 100644 recipes/jpeg/remove-version.patch delete mode 100644 recipes/numpy/__init__.py delete mode 100644 recipes/numpy/patches/add_libm_explicitly_to_build.patch delete mode 100644 recipes/numpy/patches/ranlib.patch delete mode 100644 recipes/numpy/patches/remove-default-paths.patch create mode 100644 recipes/pyjnius/__init__.py create mode 100644 recipes/pyjnius/genericndkbuild_jnienv_getter.patch create mode 100644 recipes/pyjnius/sdl3_jnienv_getter.patch create mode 100644 recipes/pyjnius/use_cython.patch create mode 100644 recipes/python3/__init__.py create mode 100644 recipes/python3/patches/cpython-311-ctypes-find-library.patch create mode 100644 recipes/python3/patches/py3.7.1_fix-ctypes-util-find-library.patch create mode 100644 recipes/python3/patches/py3.7.1_fix-zlib-version.patch create mode 100644 recipes/python3/patches/py3.7.1_fix_cortex_a8.patch create mode 100644 recipes/python3/patches/py3.8.1.patch create mode 100644 recipes/python3/patches/py3.8.1_fix_cortex_a8.patch create mode 100644 recipes/python3/patches/pyconfig_detection.patch create mode 100644 recipes/python3/patches/reproducible-buildinfo.diff create mode 100644 recipes/sqlite3/Android.mk create mode 100644 recipes/sqlite3/__init__.py diff --git a/recipes/able_recipe/__init__.py b/recipes/able_recipe/__init__.py new file mode 100644 index 0000000..3040fd3 --- /dev/null +++ b/recipes/able_recipe/__init__.py @@ -0,0 +1,37 @@ +""" +Android Bluetooth Low Energy +""" +from pythonforandroid.recipe import PythonRecipe +from pythonforandroid.toolchain import current_directory, info, shprint +import sh +from os.path import join + + +class AbleRecipe(PythonRecipe): + name = 'able_recipe' + depends = ['python3', 'setuptools', 'android'] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + def prepare_build_dir(self, arch): + build_dir = self.get_build_dir(arch) + assert build_dir.endswith(self.name) + shprint(sh.rm, '-rf', build_dir) + shprint(sh.mkdir, build_dir) + + srcs = ('../../libs/able/able', 'setup.py') + + for filename in srcs: + print(f"Copy {join(self.get_recipe_dir(), filename)} to {build_dir}") + shprint(sh.cp, '-a', join(self.get_recipe_dir(), filename), + build_dir) + + def postbuild_arch(self, arch): + super(AbleRecipe, self).postbuild_arch(arch) + info('Copying able java class to classes build dir') + with current_directory(self.get_build_dir(arch.arch)): + shprint(sh.cp, '-a', join('able', 'src', 'org'), + self.ctx.javaclass_dir) + + +recipe = AbleRecipe() diff --git a/recipes/able_recipe/setup.py b/recipes/able_recipe/setup.py new file mode 100644 index 0000000..c2ca462 --- /dev/null +++ b/recipes/able_recipe/setup.py @@ -0,0 +1,9 @@ +from setuptools import setup + +setup( + name='able', + version='0.0.0', + packages=['able', 'able.android'], + description='Bluetooth Low Energy for Android', + license='MIT', +) diff --git a/recipes/codec2/__init__.py b/recipes/codec2/__init__.py index 81aa527..f8712df 100644 --- a/recipes/codec2/__init__.py +++ b/recipes/codec2/__init__.py @@ -2,6 +2,7 @@ from os.path import join from pythonforandroid.recipe import Recipe from pythonforandroid.toolchain import current_directory, shprint import sh +import os # For debugging, clean with # buildozer android p4a -- clean_recipe_build codec2 --local-recipes ~/Information/Source/Sideband/recipes diff --git a/recipes/cython/__init__.py b/recipes/cython/__init__.py new file mode 100644 index 0000000..1256edf --- /dev/null +++ b/recipes/cython/__init__.py @@ -0,0 +1,14 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class CythonRecipe(CompiledComponentsPythonRecipe): + + version = '3.1.6' + url = 'https://github.com/cython/cython/archive/{version}.tar.gz' + site_packages_name = 'cython' + depends = ['setuptools'] + call_hostpython_via_targetpython = False + install_in_hostpython = True + + +recipe = CythonRecipe() \ No newline at end of file diff --git a/recipes/ffpyplayer/__init__.py b/recipes/ffpyplayer/__init__.py index 03960b7..53a9d7f 100644 --- a/recipes/ffpyplayer/__init__.py +++ b/recipes/ffpyplayer/__init__.py @@ -1,1483 +1,9 @@ +from pythonforandroid.recipe import PyProjectRecipe, Recipe from os.path import join -from os.path import basename, dirname, exists, isdir, isfile, join, realpath, split -import glob - -import hashlib -from re import match - -import sh -import shutil -import fnmatch -import zipfile -import urllib.request -from urllib.request import urlretrieve -from os import listdir, unlink, environ, curdir, walk -from sys import stdout -from wheel.wheelfile import WheelFile -from wheel.cli.tags import tags as wheel_tags -import time -try: - from urlparse import urlparse -except ImportError: - from urllib.parse import urlparse - -import packaging.version - -from pythonforandroid.logger import ( - logger, info, warning, debug, shprint, info_main, error) -from pythonforandroid.util import ( - current_directory, ensure_dir, BuildInterruptingException, rmdir, move, - touch) -from pythonforandroid.util import load_source as import_recipe - - -url_opener = urllib.request.build_opener() -url_orig_headers = url_opener.addheaders -urllib.request.install_opener(url_opener) - - -class RecipeMeta(type): - def __new__(cls, name, bases, dct): - if name != 'Recipe': - if 'url' in dct: - dct['_url'] = dct.pop('url') - if 'version' in dct: - dct['_version'] = dct.pop('version') - - return super().__new__(cls, name, bases, dct) - - -class Recipe(metaclass=RecipeMeta): - _url = None - '''The address from which the recipe may be downloaded. This is not - essential, it may be omitted if the source is available some other - way, such as via the :class:`IncludedFilesBehaviour` mixin. - - If the url includes the version, you may (and probably should) - replace this with ``{version}``, which will automatically be - replaced by the :attr:`version` string during download. - - .. note:: Methods marked (internal) are used internally and you - probably don't need to call them, but they are available - if you want. - ''' - - _version = None - '''A string giving the version of the software the recipe describes, - e.g. ``2.0.3`` or ``master``.''' - - md5sum = None - '''The md5sum of the source from the :attr:`url`. Non-essential, but - you should try to include this, it is used to check that the download - finished correctly. - ''' - - sha512sum = None - '''The sha512sum of the source from the :attr:`url`. Non-essential, but - you should try to include this, it is used to check that the download - finished correctly. - ''' - - blake2bsum = None - '''The blake2bsum of the source from the :attr:`url`. Non-essential, but - you should try to include this, it is used to check that the download - finished correctly. - ''' - - depends = [] - '''A list containing the names of any recipes that this recipe depends on. - ''' - - conflicts = [] - '''A list containing the names of any recipes that are known to be - incompatible with this one.''' - - opt_depends = [] - '''A list of optional dependencies, that must be built before this - recipe if they are built at all, but whose presence is not essential.''' - - patches = [] - '''A list of patches to apply to the source. Values can be either a string - referring to the patch file relative to the recipe dir, or a tuple of the - string patch file and a callable, which will receive the kwargs `arch` and - `recipe`, which should return True if the patch should be applied.''' - - python_depends = [] - '''A list of pure-Python packages that this package requires. These - packages will NOT be available at build time, but will be added to the - list of pure-Python packages to install via pip. If you need these packages - at build time, you must create a recipe.''' - - archs = ['armeabi'] # Not currently implemented properly - - built_libraries = {} - """Each recipe that builds a system library (e.g.:libffi, openssl, etc...) - should contain a dict holding the relevant information of the library. The - keys should be the generated libraries and the values the relative path of - the library inside his build folder. This dict will be used to perform - different operations: - - copy the library into the right location, depending on if it's shared - or static) - - check if we have to rebuild the library - - Here an example of how it would look like for `libffi` recipe: - - - `built_libraries = {'libffi.so': '.libs'}` - - .. note:: in case that the built library resides in recipe's build - directory, you can set the following values for the relative - path: `'.', None or ''` - """ - - need_stl_shared = False - '''Some libraries or python packages may need the c++_shared in APK. - We can automatically do this for any recipe if we set this property to - `True`''' - - stl_lib_name = 'c++_shared' - ''' - The default STL shared lib to use: `c++_shared`. - - .. note:: Android NDK version > 17 only supports 'c++_shared', because - starting from NDK r18 the `gnustl_shared` lib has been deprecated. - ''' - - def get_stl_library(self, arch): - return join( - arch.ndk_lib_dir, - 'lib{name}.so'.format(name=self.stl_lib_name), - ) - - def install_stl_lib(self, arch): - if not self.ctx.has_lib( - arch.arch, 'lib{name}.so'.format(name=self.stl_lib_name) - ): - self.install_libs(arch, self.get_stl_library(arch)) - - @property - def version(self): - key = 'VERSION_' + self.name - return environ.get(key, self._version) - - @property - def url(self): - key = 'URL_' + self.name - return environ.get(key, self._url) - - @property - def versioned_url(self): - '''A property returning the url of the recipe with ``{version}`` - replaced by the :attr:`url`. If accessing the url, you should use this - property, *not* access the url directly.''' - if self.url is None: - return None - return self.url.format(version=self.version) - - def download_file(self, url, target, cwd=None): - """ - (internal) Download an ``url`` to a ``target``. - """ - if not url: - return - - info('Downloading {} from {}'.format(self.name, url)) - - if cwd: - target = join(cwd, target) - - parsed_url = urlparse(url) - if parsed_url.scheme in ('http', 'https'): - def report_hook(index, blksize, size): - if size <= 0: - progression = '{0} bytes'.format(index * blksize) - else: - progression = '{0:.2f}%'.format( - index * blksize * 100. / float(size)) - if "CI" not in environ: - stdout.write('- Download {}\r'.format(progression)) - stdout.flush() - - if exists(target): - unlink(target) - - # Download item with multiple attempts (for bad connections): - attempts = 0 - seconds = 1 - while True: - try: - # jqueryui.com returns a 403 w/ the default user agent - # Mozilla/5.0 doesnt handle redirection for liblzma - url_opener.addheaders = [('User-agent', 'Wget/1.0')] - urlretrieve(url, target, report_hook) - except OSError as e: - attempts += 1 - if attempts >= 5: - raise - stdout.write('Download failed: {}; retrying in {} second(s)...'.format(e, seconds)) - time.sleep(seconds) - seconds *= 2 - continue - finally: - url_opener.addheaders = url_orig_headers - break - return target - elif parsed_url.scheme in ('git', 'git+file', 'git+ssh', 'git+http', 'git+https'): - if not isdir(target): - if url.startswith('git+'): - url = url[4:] - # if 'version' is specified, do a shallow clone - if self.version: - ensure_dir(target) - with current_directory(target): - shprint(sh.git, 'init') - shprint(sh.git, 'remote', 'add', 'origin', url) - else: - shprint(sh.git, 'clone', '--recursive', url, target) - with current_directory(target): - if self.version: - shprint(sh.git, 'fetch', '--tags', '--depth', '1') - shprint(sh.git, 'checkout', self.version) - branch = sh.git('branch', '--show-current') - if branch: - shprint(sh.git, 'pull') - shprint(sh.git, 'pull', '--recurse-submodules') - shprint(sh.git, 'submodule', 'update', '--recursive', '--init', '--depth', '1') - return target - - def apply_patch(self, filename, arch, build_dir=None): - """ - Apply a patch from the current recipe directory into the current - build directory. - - .. versionchanged:: 0.6.0 - Add ability to apply patch from any dir via kwarg `build_dir`''' - """ - info("Applying patch {}".format(filename)) - build_dir = build_dir if build_dir else self.get_build_dir(arch) - filename = join(self.get_recipe_dir(), filename) - shprint(sh.patch, "-t", "-d", build_dir, "-p1", - "-i", filename, _tail=10) - - def copy_file(self, filename, dest): - info("Copy {} to {}".format(filename, dest)) - filename = join(self.get_recipe_dir(), filename) - dest = join(self.build_dir, dest) - shutil.copy(filename, dest) - - def append_file(self, filename, dest): - info("Append {} to {}".format(filename, dest)) - filename = join(self.get_recipe_dir(), filename) - dest = join(self.build_dir, dest) - with open(filename, "rb") as fd: - data = fd.read() - with open(dest, "ab") as fd: - fd.write(data) - - @property - def name(self): - '''The name of the recipe, the same as the folder containing it.''' - modname = self.__class__.__module__ - return modname.split(".", 2)[-1] - - @property - def filtered_archs(self): - '''Return archs of self.ctx that are valid build archs - for the Recipe.''' - result = [] - for arch in self.ctx.archs: - if not self.archs or (arch.arch in self.archs): - result.append(arch) - return result - - def check_recipe_choices(self): - '''Checks what recipes are being built to see which of the alternative - and optional dependencies are being used, - and returns a list of these.''' - recipes = [] - built_recipes = self.ctx.recipe_build_order - for recipe in self.depends: - if isinstance(recipe, (tuple, list)): - for alternative in recipe: - if alternative in built_recipes: - recipes.append(alternative) - break - for recipe in self.opt_depends: - if recipe in built_recipes: - recipes.append(recipe) - return sorted(recipes) - - def get_opt_depends_in_list(self, recipes): - '''Given a list of recipe names, returns those that are also in - self.opt_depends. - ''' - return [recipe for recipe in recipes if recipe in self.opt_depends] - - def get_build_container_dir(self, arch): - '''Given the arch name, returns the directory where it will be - built. - - This returns a different directory depending on what - alternative or optional dependencies are being built. - ''' - dir_name = self.get_dir_name() - return join(self.ctx.build_dir, 'other_builds', - dir_name, '{}__ndk_target_{}'.format(arch, self.ctx.ndk_api)) - - def get_dir_name(self): - choices = self.check_recipe_choices() - dir_name = '-'.join([self.name] + choices) - return dir_name - - def get_build_dir(self, arch): - '''Given the arch name, returns the directory where the - downloaded/copied package will be built.''' - - return join(self.get_build_container_dir(arch), self.name) - - def get_recipe_dir(self): - """ - Returns the local recipe directory or defaults to the core recipe - directory. - """ - if self.ctx.local_recipes is not None: - local_recipe_dir = join(self.ctx.local_recipes, self.name) - if exists(local_recipe_dir): - return local_recipe_dir - return join(self.ctx.root_dir, 'recipes', self.name) - - # Public Recipe API to be subclassed if needed - - def download_if_necessary(self): - info_main('Downloading {}'.format(self.name)) - user_dir = environ.get('P4A_{}_DIR'.format(self.name.lower())) - if user_dir is not None: - info('P4A_{}_DIR is set, skipping download for {}'.format( - self.name, self.name)) - return - self.download() - - def download(self): - if self.url is None: - info('Skipping {} download as no URL is set'.format(self.name)) - return - - url = self.versioned_url - expected_digests = {} - for alg in set(hashlib.algorithms_guaranteed) | set(('md5', 'sha512', 'blake2b')): - expected_digest = getattr(self, alg + 'sum') if hasattr(self, alg + 'sum') else None - ma = match(u'^(.+)#' + alg + u'=([0-9a-f]{32,})$', url) - if ma: # fragmented URL? - if expected_digest: - raise ValueError( - ('Received {}sum from both the {} recipe ' - 'and its url').format(alg, self.name)) - url = ma.group(1) - expected_digest = ma.group(2) - if expected_digest: - expected_digests[alg] = expected_digest - - ensure_dir(join(self.ctx.packages_path, self.name)) - - with current_directory(join(self.ctx.packages_path, self.name)): - filename = shprint(sh.basename, url).stdout[:-1].decode('utf-8') - - do_download = True - marker_filename = '.mark-{}'.format(filename) - if exists(filename) and isfile(filename): - if not exists(marker_filename): - shprint(sh.rm, filename) - else: - for alg, expected_digest in expected_digests.items(): - current_digest = algsum(alg, filename) - if current_digest != expected_digest: - debug('* Generated {}sum: {}'.format(alg, - current_digest)) - debug('* Expected {}sum: {}'.format(alg, - expected_digest)) - raise ValueError( - ('Generated {0}sum does not match expected {0}sum ' - 'for {1} recipe').format(alg, self.name)) - do_download = False - - # If we got this far, we will download - if do_download: - debug('Downloading {} from {}'.format(self.name, url)) - - shprint(sh.rm, '-f', marker_filename) - self.download_file(self.versioned_url, filename) - touch(marker_filename) - - if exists(filename) and isfile(filename): - for alg, expected_digest in expected_digests.items(): - current_digest = algsum(alg, filename) - if current_digest != expected_digest: - debug('* Generated {}sum: {}'.format(alg, - current_digest)) - debug('* Expected {}sum: {}'.format(alg, - expected_digest)) - raise ValueError( - ('Generated {0}sum does not match expected {0}sum ' - 'for {1} recipe').format(alg, self.name)) - else: - info('{} download already cached, skipping'.format(self.name)) - - def unpack(self, arch): - info_main('Unpacking {} for {}'.format(self.name, arch)) - - build_dir = self.get_build_container_dir(arch) - - user_dir = environ.get('P4A_{}_DIR'.format(self.name.lower())) - if user_dir is not None: - info('P4A_{}_DIR exists, symlinking instead'.format( - self.name.lower())) - if exists(self.get_build_dir(arch)): - return - rmdir(build_dir) - ensure_dir(build_dir) - shprint(sh.cp, '-a', user_dir, self.get_build_dir(arch)) - return - - if self.url is None: - info('Skipping {} unpack as no URL is set'.format(self.name)) - return - - filename = shprint( - sh.basename, self.versioned_url).stdout[:-1].decode('utf-8') - ma = match(u'^(.+)#[a-z0-9_]{3,}=([0-9a-f]{32,})$', filename) - if ma: # fragmented URL? - filename = ma.group(1) - - with current_directory(build_dir): - directory_name = self.get_build_dir(arch) - - if not exists(directory_name) or not isdir(directory_name): - extraction_filename = join( - self.ctx.packages_path, self.name, filename) - if isfile(extraction_filename): - if extraction_filename.endswith(('.zip', '.whl')): - try: - sh.unzip(extraction_filename) - except (sh.ErrorReturnCode_1, sh.ErrorReturnCode_2): - # return code 1 means unzipping had - # warnings but did complete, - # apparently happens sometimes with - # github zips - pass - fileh = zipfile.ZipFile(extraction_filename, 'r') - root_directory = fileh.filelist[0].filename.split('/')[0] - if root_directory != basename(directory_name): - move(root_directory, directory_name) - elif extraction_filename.endswith( - ('.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz', '.txz')): - sh.tar('xf', extraction_filename) - root_directory = sh.tar('tf', extraction_filename).stdout.decode( - 'utf-8').split('\n')[0].split('/')[0] - if root_directory != basename(directory_name): - move(root_directory, directory_name) - else: - raise Exception( - 'Could not extract {} download, it must be .zip, ' - '.tar.gz or .tar.bz2 or .tar.xz'.format(extraction_filename)) - elif isdir(extraction_filename): - ensure_dir(directory_name) - 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', - join(extraction_filename, entry), - directory_name) - else: - raise Exception( - 'Given path is neither a file nor a directory: {}' - .format(extraction_filename)) - - else: - info('{} is already unpacked, skipping'.format(self.name)) - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - """Return the env specialized for the recipe - """ - if arch is None: - arch = self.filtered_archs[0] - env = arch.get_env(with_flags_in_cc=with_flags_in_cc) - return env - - def prebuild_arch(self, arch): - '''Run any pre-build tasks for the Recipe. By default, this checks if - any prebuild_archname methods exist for the archname of the current - architecture, and runs them if so.''' - prebuild = "prebuild_{}".format(arch.arch.replace('-', '_')) - if hasattr(self, prebuild): - getattr(self, prebuild)() - else: - info('{} has no {}, skipping'.format(self.name, prebuild)) - - def is_patched(self, arch): - build_dir = self.get_build_dir(arch.arch) - return exists(join(build_dir, '.patched')) - - def apply_patches(self, arch, build_dir=None): - '''Apply any patches for the Recipe. - - .. versionchanged:: 0.6.0 - Add ability to apply patches from any dir via kwarg `build_dir`''' - if self.patches: - info_main('Applying patches for {}[{}]' - .format(self.name, arch.arch)) - - if self.is_patched(arch): - info_main('{} already patched, skipping'.format(self.name)) - return - - build_dir = build_dir if build_dir else self.get_build_dir(arch.arch) - for patch in self.patches: - if isinstance(patch, (tuple, list)): - patch, patch_check = patch - if not patch_check(arch=arch, recipe=self): - continue - - self.apply_patch( - patch.format(version=self.version, arch=arch.arch), - arch.arch, build_dir=build_dir) - - touch(join(build_dir, '.patched')) - - def should_build(self, arch): - '''Should perform any necessary test and return True only if it needs - building again. Per default we implement a library test, in case that - we detect so. - - ''' - if self.built_libraries: - return not all( - exists(lib) for lib in self.get_libraries(arch.arch) - ) - return True - - def build_arch(self, arch): - '''Run any build tasks for the Recipe. By default, this checks if - any build_archname methods exist for the archname of the current - architecture, and runs them if so.''' - build = "build_{}".format(arch.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 - `built_libraries`, we will copy all defined libraries into the - right location. - ''' - if not self.built_libraries: - return - shared_libs = [ - lib for lib in self.get_libraries(arch) if lib.endswith(".so") - ] - self.install_libs(arch, *shared_libs) - - def postbuild_arch(self, arch): - '''Run any post-build tasks for the Recipe. By default, this checks if - any postbuild_archname methods exist for the archname of the - current architecture, and runs them if so. - ''' - postbuild = "postbuild_{}".format(arch.arch) - if hasattr(self, postbuild): - getattr(self, postbuild)() - - if self.need_stl_shared: - self.install_stl_lib(arch) - - def prepare_build_dir(self, arch): - '''Copies the recipe data into a build dir for the given arch. By - default, this unpacks a downloaded recipe. You should override - it (or use a Recipe subclass with different behaviour) if you - want to do something else. - ''' - self.unpack(arch) - - def clean_build(self, arch=None): - '''Deletes all the build information of the recipe. - - If arch is not None, only this arch dir is deleted. Otherwise - (the default) all builds for all archs are deleted. - - By default, this just deletes the main build dir. If the - recipe has e.g. object files biglinked, or .so files stored - elsewhere, you should override this method. - - This method is intended for testing purposes, it may have - strange results. Rebuild everything if this seems to happen. - - ''' - if arch is None: - base_dir = join(self.ctx.build_dir, 'other_builds', self.name) - else: - base_dir = self.get_build_container_dir(arch) - dirs = glob.glob(base_dir + '-*') - if exists(base_dir): - dirs.append(base_dir) - if not dirs: - warning('Attempted to clean build for {} but found no existing ' - 'build dirs'.format(self.name)) - - for directory in dirs: - rmdir(directory) - - # Delete any Python distributions to ensure the recipe build - # doesn't persist in site-packages - rmdir(self.ctx.python_installs_dir) - - def install_libs(self, arch, *libs): - libs_dir = self.ctx.get_libs_dir(arch.arch) - if not libs: - warning('install_libs called with no libraries to install!') - return - args = libs + (libs_dir,) - shprint(sh.cp, *args) - - def has_libs(self, arch, *libs): - return all(map(lambda lib: self.ctx.has_lib(arch.arch, lib), libs)) - - def get_libraries(self, arch_name, in_context=False): - """Return the full path of the library depending on the architecture. - Per default, the build library path it will be returned, unless - `get_libraries` has been called with kwarg `in_context` set to - True. - - .. note:: this method should be used for library recipes only - """ - recipe_libs = set() - if not self.built_libraries: - return recipe_libs - for lib, rel_path in self.built_libraries.items(): - if not in_context: - abs_path = join(self.get_build_dir(arch_name), rel_path, lib) - if rel_path in {".", "", None}: - abs_path = join(self.get_build_dir(arch_name), lib) - else: - abs_path = join(self.ctx.get_libs_dir(arch_name), lib) - recipe_libs.add(abs_path) - return recipe_libs - - @classmethod - def recipe_dirs(cls, ctx): - recipe_dirs = [] - if ctx.local_recipes is not None: - recipe_dirs.append(realpath(ctx.local_recipes)) - if ctx.storage_dir: - recipe_dirs.append(join(ctx.storage_dir, 'recipes')) - recipe_dirs.append(join(ctx.root_dir, "recipes")) - return recipe_dirs - - @classmethod - def list_recipes(cls, ctx): - forbidden_dirs = ('__pycache__', ) - for recipes_dir in cls.recipe_dirs(ctx): - if recipes_dir and exists(recipes_dir): - for name in listdir(recipes_dir): - if name in forbidden_dirs: - continue - fn = join(recipes_dir, name) - if isdir(fn): - yield name - - @classmethod - def get_recipe(cls, name, ctx): - '''Returns the Recipe with the given name, if it exists.''' - name = name.lower() - if not hasattr(cls, "recipes"): - cls.recipes = {} - if name in cls.recipes: - return cls.recipes[name] - - recipe_file = None - for recipes_dir in cls.recipe_dirs(ctx): - if not exists(recipes_dir): - continue - # Find matching folder (may differ in case): - for subfolder in listdir(recipes_dir): - if subfolder.lower() == name: - recipe_file = join(recipes_dir, subfolder, '__init__.py') - if exists(recipe_file): - name = subfolder # adapt to actual spelling - break - recipe_file = None - if recipe_file is not None: - break - - else: - raise ValueError('Recipe does not exist: {}'.format(name)) - - mod = import_recipe('pythonforandroid.recipes.{}'.format(name), recipe_file) - if len(logger.handlers) > 1: - logger.removeHandler(logger.handlers[1]) - recipe = mod.recipe - recipe.ctx = ctx - cls.recipes[name.lower()] = recipe - return recipe - - -class IncludedFilesBehaviour(object): - '''Recipe mixin class that will automatically unpack files included in - the recipe directory.''' - src_filename = None - - def prepare_build_dir(self, arch): - if self.src_filename is None: - raise BuildInterruptingException( - 'IncludedFilesBehaviour failed: no src_filename specified') - rmdir(self.get_build_dir(arch)) - shprint(sh.cp, '-a', join(self.get_recipe_dir(), self.src_filename), - self.get_build_dir(arch)) - - -class BootstrapNDKRecipe(Recipe): - '''A recipe class for recipes built in an Android project jni dir with - an Android.mk. These are not cached separatly, but built in the - bootstrap's own building directory. - - To build an NDK project which is not part of the bootstrap, see - :class:`~pythonforandroid.recipe.NDKRecipe`. - - To link with python, call the method :meth:`get_recipe_env` - with the kwarg *with_python=True*. - ''' - - dir_name = None # The name of the recipe build folder in the jni dir - - def get_build_container_dir(self, arch): - return self.get_jni_dir() - - def get_build_dir(self, arch): - if self.dir_name is None: - raise ValueError('{} recipe doesn\'t define a dir_name, but ' - 'this is necessary'.format(self.name)) - return join(self.get_build_container_dir(arch), self.dir_name) - - def get_jni_dir(self): - return join(self.ctx.bootstrap.build_dir, 'jni') - - def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=False): - env = super().get_recipe_env(arch, with_flags_in_cc) - if not with_python: - return env - - env['PYTHON_INCLUDE_ROOT'] = self.ctx.python_recipe.include_root(arch.arch) - env['PYTHON_LINK_ROOT'] = self.ctx.python_recipe.link_root(arch.arch) - env['EXTRA_LDLIBS'] = ' -lpython{}'.format( - self.ctx.python_recipe.link_version) - return env - - -class NDKRecipe(Recipe): - '''A recipe class for any NDK project not included in the bootstrap.''' - - generated_libraries = [] - - def should_build(self, arch): - lib_dir = self.get_lib_dir(arch) - - for lib in self.generated_libraries: - if not exists(join(lib_dir, lib)): - return True - - return False - - def get_lib_dir(self, arch): - return join(self.get_build_dir(arch.arch), 'obj', 'local', arch.arch) - - def get_jni_dir(self, arch): - return join(self.get_build_dir(arch.arch), 'jni') - - def build_arch(self, arch, *extra_args): - super().build_arch(arch) - - env = self.get_recipe_env(arch) - with current_directory(self.get_build_dir(arch.arch)): - shprint( - sh.Command(join(self.ctx.ndk_dir, "ndk-build")), - 'V=1', - 'NDK_DEBUG=' + ("1" if self.ctx.build_as_debuggable else "0"), - 'APP_PLATFORM=android-' + str(self.ctx.ndk_api), - 'APP_ABI=' + arch.arch, - *extra_args, _env=env - ) - - -class PythonRecipe(Recipe): - site_packages_name = None - '''The name of the module's folder when installed in the Python - site-packages (e.g. for pyjnius it is 'jnius')''' - - call_hostpython_via_targetpython = True - '''If True, tries to install the module using the hostpython binary - copied to the target (normally arm) python build dir. However, this - will fail if the module tries to import e.g. _io.so. Set this to False - to call hostpython from its own build dir, installing the module in - the right place via arguments to setup.py. However, this may not set - the environment correctly and so False is not the default.''' - - install_in_hostpython = False - '''If True, additionally installs the module in the hostpython build - dir. This will make it available to other recipes if - call_hostpython_via_targetpython is False. - ''' - - install_in_targetpython = True - '''If True, installs the module in the targetpython installation dir. - This is almost always what you want to do.''' - - setup_extra_args = [] - '''List of extra arguments to pass to setup.py''' - - depends = ['python3'] - ''' - .. note:: it's important to keep this depends as a class attribute outside - `__init__` because sometimes we only initialize the class, so the - `__init__` call won't be called and the deps would be missing - (which breaks the dependency graph computation) - - .. warning:: don't forget to call `super().__init__()` in any recipe's - `__init__`, or otherwise it may not be ensured that it depends - on python2 or python3 which can break the dependency graph - ''' - - hostpython_prerequisites = [] - '''List of hostpython packages required to build a recipe''' - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if 'python3' not in self.depends: - # We ensure here that the recipe depends on python even it overrode - # `depends`. We only do this if it doesn't already depend on any - # python, since some recipes intentionally don't depend on/work - # with all python variants - depends = self.depends - depends.append('python3') - depends = list(set(depends)) - self.depends = depends - - def clean_build(self, arch=None): - super().clean_build(arch=arch) - name = self.folder_name - python_install_dirs = glob.glob(join(self.ctx.python_installs_dir, '*')) - for python_install in python_install_dirs: - site_packages_dir = glob.glob(join(python_install, 'lib', 'python*', - 'site-packages')) - if site_packages_dir: - build_dir = join(site_packages_dir[0], name) - if exists(build_dir): - info('Deleted {}'.format(build_dir)) - rmdir(build_dir) - - @property - 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 - else: - python_recipe = self.ctx.python_recipe - return 'python{}'.format(python_recipe.version) - - @property - def hostpython_location(self): - if not self.call_hostpython_via_targetpython: - return self.real_hostpython_location - return self.ctx.hostpython - - @property - def folder_name(self): - '''The name of the build folders containing this recipe.''' - name = self.site_packages_name - if name is None: - name = self.name - return name - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - 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"] - - if not self.call_hostpython_via_targetpython: - env['CFLAGS'] += ' -I{}'.format( - self.ctx.python_recipe.include_root(arch.arch) - ) - env['LDFLAGS'] += ' -L{} -lpython{}'.format( - 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): - info('Python package already exists in site-packages') - return False - info('{} apparently isn\'t already in site-packages'.format(name)) - return True - - def build_arch(self, arch): - '''Install the Python module by calling setup.py install with - the target Python dir.''' - self.install_hostpython_prerequisites() - super().build_arch(arch) - self.install_python_package(arch) - - def install_python_package(self, arch, name=None, env=None, is_dir=True): - '''Automate the installation of a Python package (or a cython - package where the cython components are pre-built).''' - # arch = self.filtered_archs[0] # old kivy-ios way - if name is None: - name = self.name - if env is None: - env = self.get_recipe_env(arch) - - info('Installing {} into site-packages'.format(self.name)) - - 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 asked, also install in the hostpython build dir - if self.install_in_hostpython: - self.install_hostpython_package(arch) - - def get_hostrecipe_env(self, arch): - env = environ.copy() - env['PYTHONPATH'] = self.hostpython_site_dir - 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) - 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', - _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): - if not packages: - packages = self.hostpython_prerequisites - - if len(packages) == 0: - return - - pip_options = [ - "install", - *packages, - "--target", self.hostpython_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) - - def restore_hostpython_prerequisites(self, packages): - _packages = [] - for package in packages: - original_version = Recipe.get_recipe(package, self.ctx).version - _packages.append(package + "==" + original_version) - self.install_hostpython_prerequisites(packages=_packages) - - -class CompiledComponentsPythonRecipe(PythonRecipe): - pre_build_ext = False - - build_cmd = 'build_ext' - - def build_arch(self, arch): - '''Build any cython components, then install the Python module by - calling setup.py install with the target Python dir. - ''' - Recipe.build_arch(self, arch) - self.install_hostpython_prerequisites() - self.build_compiled_components(arch) - self.install_python_package(arch) - - def build_compiled_components(self, arch): - info('Building compiled components in {}'.format(self.name)) - - env = self.get_recipe_env(arch) - hostpython = sh.Command(self.hostpython_location) - with current_directory(self.get_build_dir(arch.arch)): - if self.install_in_hostpython: - shprint(hostpython, 'setup.py', 'clean', '--all', _env=env) - shprint(hostpython, 'setup.py', self.build_cmd, '-v', - _env=env, *self.setup_extra_args) - build_dir = glob.glob('build/lib.*')[0] - shprint(sh.find, build_dir, '-name', '"*.o"', '-exec', - env['STRIP'], '{}', ';', _env=env) - - def install_hostpython_package(self, arch): - env = self.get_hostrecipe_env(arch) - 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, - *self.setup_extra_args) - - -class CppCompiledComponentsPythonRecipe(CompiledComponentsPythonRecipe): - """ Extensions that require the cxx-stl """ - call_hostpython_via_targetpython = False - need_stl_shared = True - - -class CythonRecipe(PythonRecipe): - pre_build_ext = False - cythonize = True - cython_args = [] - call_hostpython_via_targetpython = False - - def build_arch(self, arch): - '''Build any cython components, then install the Python module by - calling setup.py install with the target Python dir. - ''' - Recipe.build_arch(self, arch) - self.build_cython_components(arch) - self.install_python_package(arch) - - def build_cython_components(self, arch): - info('Cythonizing anything necessary in {}'.format(self.name)) - - env = self.get_recipe_env(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) - debug('cwd is {}'.format(realpath(curdir))) - info('Trying first build of {} to get cython files: this is ' - 'expected to fail'.format(self.name)) - - manually_cythonise = False - try: - shprint(hostpython, 'setup.py', 'build_ext', '-v', _env=env, - *self.setup_extra_args) - except sh.ErrorReturnCode_1: - print() - info('{} first build failed (as expected)'.format(self.name)) - manually_cythonise = True - - if manually_cythonise: - self.cythonize_build(env=env) - shprint(hostpython, 'setup.py', 'build_ext', '-v', _env=env, - _tail=20, _critical=True, *self.setup_extra_args) - else: - info('First build appeared to complete correctly, skipping manual' - 'cythonising.') - - if not self.ctx.with_debug_symbols: - self.strip_object_files(arch, env) - - def strip_object_files(self, arch, env, build_dir=None): - if build_dir is None: - build_dir = self.get_build_dir(arch.arch) - with current_directory(build_dir): - info('Stripping object files') - shprint(sh.find, '.', '-iname', '*.so', '-exec', - '/usr/bin/echo', '{}', ';', _env=env) - shprint(sh.find, '.', '-iname', '*.so', '-exec', - env['STRIP'].split(' ')[0], '--strip-unneeded', - # '/usr/bin/strip', '--strip-unneeded', - '{}', ';', _env=env) - - def cythonize_file(self, env, build_dir, filename): - short_filename = filename - if filename.startswith(build_dir): - short_filename = filename[len(build_dir) + 1:] - info(u"Cythonize {}".format(short_filename)) - cyenv = env.copy() - if 'CYTHONPATH' in cyenv: - cyenv['PYTHONPATH'] = cyenv['CYTHONPATH'] - elif 'PYTHONPATH' in cyenv: - del cyenv['PYTHONPATH'] - if 'PYTHONNOUSERSITE' in cyenv: - cyenv.pop('PYTHONNOUSERSITE') - python_command = sh.Command("python{}".format( - self.ctx.python_recipe.major_minor_version_string.split(".")[0] - )) - shprint(python_command, "-c" - "import sys; from Cython.Compiler.Main import setuptools_main; sys.exit(setuptools_main());", - filename, *self.cython_args, _env=cyenv) - - def cythonize_build(self, env, build_dir="."): - if not self.cythonize: - info('Running cython cancelled per recipe setting') - return - info('Running cython where appropriate') - for root, dirnames, filenames in walk("."): - for filename in fnmatch.filter(filenames, "*.pyx"): - self.cythonize_file(env, build_dir, join(root, filename)) - - def get_recipe_env(self, arch, with_flags_in_cc=True): - env = super().get_recipe_env(arch, with_flags_in_cc) - env['LDFLAGS'] = env['LDFLAGS'] + ' -L{} '.format( - self.ctx.get_libs_dir(arch.arch) + - ' -L{} '.format(self.ctx.libs_dir) + - ' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'obj', 'local', - arch.arch))) - - env['LDSHARED'] = env['CC'] + ' -shared' - # shprint(sh.whereis, env['LDSHARED'], _env=env) - env['LIBLINK'] = 'NOTNONE' - if self.ctx.copy_libs: - env['COPYLIBS'] = '1' - - # Every recipe uses its own liblink path, object files are - # collected and biglinked later - liblink_path = join(self.get_build_container_dir(arch.arch), - 'objects_{}'.format(self.name)) - env['LIBLINK_PATH'] = liblink_path - ensure_dir(liblink_path) - - return env - - -class PyProjectRecipe(PythonRecipe): - '''Recipe for projects which containes `pyproject.toml`''' - - # Extra args to pass to `python -m build ...` - extra_build_args = [] - call_hostpython_via_targetpython = False - - def get_recipe_env(self, arch, **kwargs): - # Custom hostpython - self.ctx.python_recipe.python_exe = join( - self.ctx.python_recipe.get_build_dir(arch), "android-build", "python3") - env = super().get_recipe_env(arch, **kwargs) - build_dir = self.get_build_dir(arch) - ensure_dir(build_dir) - build_opts = join(build_dir, "build-opts.cfg") - - with open(build_opts, "w") as file: - file.write("[bdist_wheel]\nplat-name={}".format( - self.get_wheel_platform_tag(arch) - )) - file.close() - - env["DIST_EXTRA_CONFIG"] = build_opts - return env - - def get_wheel_platform_tag(self, arch): - return "android_" + { - "armeabi-v7a": "arm", - "arm64-v8a": "aarch64", - "x86_64": "x86_64", - "x86": "i686", - }[arch.arch] - - def install_wheel(self, arch, built_wheels): - _wheel = built_wheels[0] - built_wheel_dir = dirname(_wheel) - # Fix wheel platform tag - wheel_tag = wheel_tags( - _wheel, - platform_tags=self.get_wheel_platform_tag(arch), - remove=True, - ) - 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) - - 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( - packages=["build[virtualenv]", "pip"] + self.hostpython_prerequisites - ) - build_dir = self.get_build_dir(arch.arch) - env = self.get_recipe_env(arch, with_flags_in_cc=True) - # make build dir separatly - sub_build_dir = join(build_dir, "p4a_android_build") - ensure_dir(sub_build_dir) - # copy hostpython to built python to ensure correct selection of libs and includes - shprint(sh.cp, self.real_hostpython_location, self.ctx.python_recipe.python_exe) - - build_args = [ - "-m", - "build", - "--wheel", - "--config-setting", - "builddir={}".format(sub_build_dir), - ] + self.extra_build_args - - built_wheels = [] - with current_directory(build_dir): - shprint( - 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) - - -class MesonRecipe(PyProjectRecipe): - '''Recipe for projects which uses meson as build system''' - - meson_version = "1.4.0" - ninja_version = "1.11.1.1" - - def sanitize_flags(self, *flag_strings): - return " ".join(flag_strings).strip().split(" ") - - def get_recipe_meson_options(self, arch): - env = self.get_recipe_env(arch, with_flags_in_cc=True) - return { - "binaries": { - "c": arch.get_clang_exe(with_target=True), - "cpp": arch.get_clang_exe(with_target=True, plus_plus=True), - "ar": self.ctx.ndk.llvm_ar, - "strip": self.ctx.ndk.llvm_strip, - }, - "built-in options": { - "c_args": self.sanitize_flags(env["CFLAGS"], env["CPPFLAGS"]), - "cpp_args": self.sanitize_flags(env["CXXFLAGS"], env["CPPFLAGS"]), - "c_link_args": self.sanitize_flags(env["LDFLAGS"]), - "cpp_link_args": self.sanitize_flags(env["LDFLAGS"]), - }, - "properties": { - "needs_exe_wrapper": True, - "sys_root": self.ctx.ndk.sysroot - }, - "host_machine": { - "cpu_family": { - "arm64-v8a": "aarch64", - "armeabi-v7a": "arm", - "x86_64": "x86_64", - "x86": "x86" - }[arch.arch], - "cpu": { - "arm64-v8a": "aarch64", - "armeabi-v7a": "armv7", - "x86_64": "x86_64", - "x86": "i686" - }[arch.arch], - "endian": "little", - "system": "android", - } - } - - def write_build_options(self, arch): - """Writes python dict to meson config file""" - option_data = "" - build_options = self.get_recipe_meson_options(arch) - for key in build_options.keys(): - data_chunk = "[{}]".format(key) - for subkey in build_options[key].keys(): - value = build_options[key][subkey] - if isinstance(value, int): - value = str(value) - elif isinstance(value, str): - value = "'{}'".format(value) - elif isinstance(value, bool): - value = "true" if value else "false" - elif isinstance(value, list): - value = "['" + "', '".join(value) + "']" - data_chunk += "\n" + subkey + " = " + value - option_data += data_chunk + "\n\n" - return option_data - - def ensure_args(self, *args): - for arg in args: - if arg not in self.extra_build_args: - self.extra_build_args.append(arg) - - def build_arch(self, arch): - cross_file = join("/tmp", "android.meson.cross") - info("Writing cross file at: {}".format(cross_file)) - # write cross config file - with open(cross_file, "w") as file: - file.write(self.write_build_options(arch)) - file.close() - # set cross file - self.ensure_args('-Csetup-args=--cross-file', '-Csetup-args={}'.format(cross_file)) - # ensure ninja and meson - for dep in [ - "ninja=={}".format(self.ninja_version), - "meson=={}".format(self.meson_version), - ]: - if dep not in self.hostpython_prerequisites: - self.hostpython_prerequisites.append(dep) - super().build_arch(arch) - - -class RustCompiledComponentsRecipe(PyProjectRecipe): - # Rust toolchain codes - # https://doc.rust-lang.org/nightly/rustc/platform-support.html - RUST_ARCH_CODES = { - "arm64-v8a": "aarch64-linux-android", - "armeabi-v7a": "armv7-linux-androideabi", - "x86_64": "x86_64-linux-android", - "x86": "i686-linux-android", - } - - call_hostpython_via_targetpython = False - - def get_recipe_env(self, arch, **kwargs): - env = super().get_recipe_env(arch, **kwargs) - - # Set rust build target - build_target = self.RUST_ARCH_CODES[arch.arch] - cargo_linker_name = "CARGO_TARGET_{}_LINKER".format( - build_target.upper().replace("-", "_") - ) - env["CARGO_BUILD_TARGET"] = build_target - env[cargo_linker_name] = join( - self.ctx.ndk.llvm_prebuilt_dir, - "bin", - "{}{}-clang".format( - # NDK's Clang format - build_target.replace("7", "7a") - if build_target.startswith("armv7") - else build_target, - self.ctx.ndk_api, - ), - ) - realpython_dir = self.ctx.python_recipe.get_build_dir(arch.arch) - - env["RUSTFLAGS"] = "-Clink-args=-L{} -L{}".format( - self.ctx.get_libs_dir(arch.arch), join(realpython_dir, "android-build") - ) - - env["PYO3_CROSS_LIB_DIR"] = realpath(glob.glob(join( - realpython_dir, "android-build", "build", - "lib.linux-*-{}/".format(self.python_major_minor_version), - ))[0]) - - info_main("Ensuring rust build toolchain") - shprint(sh.rustup, "target", "add", build_target) - - # Add host python to PATH - env["PATH"] = ("{hostpython_dir}:{old_path}").format( - hostpython_dir=Recipe.get_recipe( - "hostpython3", self.ctx - ).get_path_to_python(), - old_path=env["PATH"], - ) - return env - - def check_host_deps(self): - if not hasattr(sh, "rustup"): - error( - "`rustup` was not found on host system." - "Please install it using :" - "\n`curl https://sh.rustup.rs -sSf | sh`\n" - ) - exit(1) - - def build_arch(self, arch): - self.check_host_deps() - super().build_arch(arch) - - -class TargetPythonRecipe(Recipe): - '''Class for target python recipes. Sets ctx.python_recipe to point to - itself, so as to know later what kind of Python was built or used.''' - - def __init__(self, *args, **kwargs): - self._ctx = None - super().__init__(*args, **kwargs) - - def prebuild_arch(self, arch): - super().prebuild_arch(arch) - self.ctx.python_recipe = self - - def include_root(self, arch): - '''The root directory from which to include headers.''' - raise NotImplementedError('Not implemented in TargetPythonRecipe') - - def link_root(self): - raise NotImplementedError('Not implemented in TargetPythonRecipe') - - @property - def major_minor_version_string(self): - parsed_version = packaging.version.parse(self.version) - return f"{parsed_version.major}.{parsed_version.minor}" - - def create_python_bundle(self, dirn, arch): - """ - Create a packaged python bundle in the target directory, by - copying all the modules and standard library to the right - place. - """ - raise NotImplementedError('{} does not implement create_python_bundle'.format(self)) - - def reduce_object_file_names(self, dirn): - """Recursively renames all files named XXX.cpython-...-linux-gnu.so" - to "XXX.so", i.e. removing the erroneous architecture name - coming from the local system. - """ - py_so_files = shprint(sh.find, dirn, '-iname', '*.so') - filens = py_so_files.stdout.decode('utf-8').split('\n')[:-1] - for filen in filens: - file_dirname, file_basename = split(filen) - parts = file_basename.split('.') - if len(parts) <= 2: - continue - # PySide6 libraries end with .abi3.so - if parts[1] == "abi3": - continue - move(filen, join(file_dirname, parts[0] + '.so')) - - -def algsum(alg, filen): - '''Calculate the digest of a file. - ''' - with open(filen, 'rb') as fileh: - digest = getattr(hashlib, alg)(fileh.read()) - - return digest.hexdigest() - class FFPyPlayerRecipe(PyProjectRecipe): - version = 'v4.5.1' + version = 'v4.5.2' url = 'https://github.com/matham/ffpyplayer/archive/{version}.zip' depends = ['python3', 'sdl2', 'ffmpeg'] patches = ["setup.py.patch"] @@ -1506,7 +32,7 @@ class FFPyPlayerRecipe(PyProjectRecipe): env['LIBLINK'] = 'NOTNONE' # ffmpeg recipe enables GPL components only if ffpyplayer_codecs recipe used. - # Therefor we need to disable libpostproc if skipped. + # Therefore we need to disable libpostproc if skipped. if 'ffpyplayer_codecs' not in self.ctx.recipe_build_order: env["CONFIG_POSTPROC"] = '0' diff --git a/recipes/ffpyplayer/setup.py.patch b/recipes/ffpyplayer/setup.py.patch index 6a7d42f..35f4b69 100644 --- a/recipes/ffpyplayer/setup.py.patch +++ b/recipes/ffpyplayer/setup.py.patch @@ -1,5 +1,5 @@ ---- ffpyplayer/setup.py 2024-06-02 11:10:49.691183467 +0530 -+++ ffpyplayer.mod/setup.py 2024-06-02 11:20:16.220966873 +0530 +--- ffpyplayer/setup.py 2024-06-02 11:10:49.691183467 +0530 ++++ ffpyplayer.mod/setup.py 2024-06-02 11:20:16.220966873 +0530 @@ -27,12 +27,6 @@ # This sets whether or not Cython gets added to setup_requires. declare_cython = False diff --git a/recipes/hostpython3/__init__.py b/recipes/hostpython3/__init__.py new file mode 100644 index 0000000..b449a30 --- /dev/null +++ b/recipes/hostpython3/__init__.py @@ -0,0 +1,175 @@ +import sh +import os + +from multiprocessing import cpu_count +from pathlib import Path +from os.path import join + +from packaging.version import Version +from pythonforandroid.logger import shprint +from pythonforandroid.recipe import Recipe +from pythonforandroid.util import ( + BuildInterruptingException, + current_directory, + ensure_dir, +) +from pythonforandroid.prerequisites import OpenSSLPrerequisite + +HOSTPYTHON_VERSION_UNSET_MESSAGE = ( + 'The hostpython recipe must have set version' +) + +SETUP_DIST_NOT_FIND_MESSAGE = ( + 'Could not find Setup.dist or Setup in Python build' +) + + +class HostPython3Recipe(Recipe): + ''' + The hostpython3's recipe. + + .. versionchanged:: 2019.10.06.post0 + Refactored from deleted class ``python.HostPythonRecipe`` into here. + + .. versionchanged:: 0.6.0 + Refactored into the new class + :class:`~pythonforandroid.python.HostPythonRecipe` + ''' + + version = '3.11.5' + name = 'hostpython3' + + 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): + ''' + Returns the name of the python executable depending on the version. + ''' + if not self.version: + raise BuildInterruptingException(HOSTPYTHON_VERSION_UNSET_MESSAGE) + return f'python{self.version.split(".")[0]}' + + @property + def python_exe(self): + '''Returns the full path of the hostpython executable.''' + return join(self.get_path_to_python(), self._exe_name) + + def get_recipe_env(self, arch=None): + env = os.environ.copy() + openssl_prereq = OpenSSLPrerequisite() + if env.get("PKG_CONFIG_PATH", ""): + env["PKG_CONFIG_PATH"] = os.pathsep.join( + [openssl_prereq.pkg_config_location, env["PKG_CONFIG_PATH"]] + ) + else: + env["PKG_CONFIG_PATH"] = openssl_prereq.pkg_config_location + return env + + def should_build(self, arch): + if Path(self.python_exe).exists(): + # no need to build, but we must set hostpython for our Context + self.ctx.hostpython = self.python_exe + return False + return True + + def get_build_container_dir(self, arch=None): + choices = self.check_recipe_choices() + dir_name = '-'.join([self.name] + choices) + return join(self.ctx.build_dir, 'other_builds', dir_name, 'desktop') + + def get_build_dir(self, arch=None): + ''' + .. note:: Unlike other recipes, the hostpython build dir doesn't + depend on the target arch + ''' + return join(self.get_build_container_dir(), self.name) + + 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): + return join(self.site_root, self.site_dir, "bin") + + @property + def local_bin(self): + return join(self.site_root, "usr/local/bin/") + + @property + def site_dir(self): + p_version = Version(self.version) + return join( + self.site_root, + f"usr/local/lib/python{p_version.major}.{p_version.minor}/site-packages/" + ) + + def build_arch(self, arch): + env = self.get_recipe_env(arch) + + recipe_build_dir = self.get_build_dir(arch.arch) + + # Create a subdirectory to actually perform the build + build_dir = join(recipe_build_dir, self.build_subdir) + ensure_dir(build_dir) + + # Configure the build + build_configured = False + with current_directory(build_dir): + if not Path('config.status').exists(): + shprint(sh.Command(join(recipe_build_dir, 'configure')), _env=env) + build_configured = True + + with current_directory(recipe_build_dir): + # Create the Setup file. This copying from Setup.dist is + # the normal and expected procedure before Python 3.8, but + # after this the file with default options is already named "Setup" + setup_dist_location = join('Modules', 'Setup.dist') + if Path(setup_dist_location).exists(): + shprint(sh.cp, setup_dist_location, + join(build_dir, 'Modules', 'Setup')) + else: + # Check the expected file does exist + setup_location = join('Modules', 'Setup') + if not Path(setup_location).exists(): + raise BuildInterruptingException( + SETUP_DIST_NOT_FIND_MESSAGE + ) + + shprint(sh.make, '-j', str(cpu_count()), '-C', build_dir, _env=env) + + # make a copy of the python executable giving it the name we want, + # because we got different python's executable names depending on + # the fs being case-insensitive (Mac OS X, Cygwin...) or + # case-sensitive (linux)...so this way we will have an unique name + # for our hostpython, regarding the used fs + for exe_name in ['python.exe', 'python']: + exe = join(self.get_path_to_python(), exe_name) + if Path(exe).is_file(): + shprint(sh.cp, exe, self.python_exe) + break + + ensure_dir(self.site_root) + self.ctx.hostpython = self.python_exe + if build_configured: + print("RUNNING ENSUREPIP:"+self.site_root) + shprint( + sh.Command(self.python_exe), "-m", "ensurepip", "--root", self.site_root, "-U", + _env={"HOME": "/tmp"} + ) + print("RAN ENSUREPIP") + + +recipe = HostPython3Recipe() \ No newline at end of file diff --git a/recipes/hostpython3/patches/pyconfig_detection.patch b/recipes/hostpython3/patches/pyconfig_detection.patch new file mode 100644 index 0000000..a19b468 --- /dev/null +++ b/recipes/hostpython3/patches/pyconfig_detection.patch @@ -0,0 +1,13 @@ +diff -Nru Python-3.8.2/Lib/site.py Python-3.8.2-new/Lib/site.py +--- Python-3.8.2/Lib/site.py 2020-04-28 12:48:38.000000000 -0700 ++++ Python-3.8.2-new/Lib/site.py 2020-04-28 12:52:46.000000000 -0700 +@@ -487,7 +487,8 @@ + if key == 'include-system-site-packages': + system_site = value.lower() + elif key == 'home': +- sys._home = value ++ # this is breaking pyconfig.h path detection with venv ++ print('Ignoring "sys._home = value" override', file=sys.stderr) + + sys.prefix = sys.exec_prefix = site_prefix + \ No newline at end of file diff --git a/recipes/jpeg/Application.mk b/recipes/jpeg/Application.mk new file mode 100644 index 0000000..5942a03 --- /dev/null +++ b/recipes/jpeg/Application.mk @@ -0,0 +1,4 @@ +APP_OPTIM := release +APP_ABI := all # or armeabi +APP_MODULES := libjpeg +APP_ALLOW_MISSING_DEPS := true diff --git a/recipes/jpeg/__init__.py b/recipes/jpeg/__init__.py new file mode 100644 index 0000000..33a9ba4 --- /dev/null +++ b/recipes/jpeg/__init__.py @@ -0,0 +1,58 @@ +from pythonforandroid.recipe import Recipe +from pythonforandroid.logger import shprint +from pythonforandroid.util import current_directory +from os.path import join +import sh + + +class JpegRecipe(Recipe): + ''' + .. versionchanged:: 0.6.0 + rewrote recipe to be build with clang and updated libraries to latest + version of the official git repo. + ''' + name = 'jpeg' + version = '2.0.1' + url = 'https://github.com/libjpeg-turbo/libjpeg-turbo/archive/{version}.tar.gz' # noqa + built_libraries = {'libjpeg.a': '.', 'libturbojpeg.a': '.'} + # we will require this below patch to build the shared library + # patches = ['remove-version.patch'] + + def build_arch(self, arch): + build_dir = self.get_build_dir(arch.arch) + + # TODO: Fix simd/neon + with current_directory(build_dir): + env = self.get_recipe_env(arch) + toolchain_file = join(self.ctx.ndk_dir, + 'build/cmake/android.toolchain.cmake') + + shprint(sh.rm, '-rf', 'CMakeCache.txt', 'CMakeFiles/') + shprint(sh.cmake, '-G', 'Unix Makefiles', + '-DCMAKE_SYSTEM_NAME=Android', + '-DCMAKE_POSITION_INDEPENDENT_CODE=1', + '-DCMAKE_ANDROID_ARCH_ABI={arch}'.format(arch=arch.arch), + '-DCMAKE_ANDROID_NDK=' + self.ctx.ndk_dir, + '-DCMAKE_C_COMPILER={cc}'.format(cc=arch.get_clang_exe()), + '-DCMAKE_CXX_COMPILER={cc_plus}'.format( + cc_plus=arch.get_clang_exe(plus_plus=True)), + '-DCMAKE_BUILD_TYPE=Release', + '-DCMAKE_INSTALL_PREFIX=./install', + '-DCMAKE_TOOLCHAIN_FILE=' + toolchain_file, + + '-DANDROID_ABI={arch}'.format(arch=arch.arch), + '-DANDROID_ARM_NEON=ON', + '-DENABLE_NEON=ON', + # '-DREQUIRE_SIMD=1', + + # Force disable shared, with the static ones is enough + '-DENABLE_SHARED=0', + '-DENABLE_STATIC=1', + + # Fix cmake compatibility issue + '-DCMAKE_POLICY_VERSION_MINIMUM=3.5', + _env=env) + shprint(sh.make, _env=env) + + +recipe = JpegRecipe() diff --git a/recipes/jpeg/build-static.patch b/recipes/jpeg/build-static.patch new file mode 100644 index 0000000..0aa9c70 --- /dev/null +++ b/recipes/jpeg/build-static.patch @@ -0,0 +1,85 @@ +diff -Naur jpeg/Android.mk b/Android.mk +--- jpeg/Android.mk 2015-12-14 11:37:25.900190235 -0600 ++++ b/Android.mk 2015-12-14 11:41:27.532182210 -0600 +@@ -54,8 +54,7 @@ + + LOCAL_SRC_FILES:= $(libjpeg_SOURCES_DIST) + +-LOCAL_SHARED_LIBRARIES := libcutils +-LOCAL_STATIC_LIBRARIES := libsimd ++LOCAL_STATIC_LIBRARIES := libsimd libcutils + + LOCAL_C_INCLUDES := $(LOCAL_PATH) + +@@ -68,7 +67,7 @@ + + LOCAL_MODULE := libjpeg + +-include $(BUILD_SHARED_LIBRARY) ++include $(BUILD_STATIC_LIBRARY) + + ###################################################### + ### cjpeg ### +@@ -82,7 +81,7 @@ + + LOCAL_SRC_FILES:= $(cjpeg_SOURCES) + +-LOCAL_SHARED_LIBRARIES := libjpeg ++LOCAL_STATIC_LIBRARIES := libjpeg + + LOCAL_C_INCLUDES := $(LOCAL_PATH) \ + $(LOCAL_PATH)/android +@@ -110,7 +109,7 @@ + + LOCAL_SRC_FILES:= $(djpeg_SOURCES) + +-LOCAL_SHARED_LIBRARIES := libjpeg ++LOCAL_STATIC_LIBRARIES := libjpeg + + LOCAL_C_INCLUDES := $(LOCAL_PATH) \ + $(LOCAL_PATH)/android +@@ -137,7 +136,7 @@ + + LOCAL_SRC_FILES:= $(jpegtran_SOURCES) + +-LOCAL_SHARED_LIBRARIES := libjpeg ++LOCAL_STATIC_LIBRARIES := libjpeg + + LOCAL_C_INCLUDES := $(LOCAL_PATH) \ + $(LOCAL_PATH)/android +@@ -163,7 +162,7 @@ + + LOCAL_SRC_FILES:= $(tjunittest_SOURCES) + +-LOCAL_SHARED_LIBRARIES := libjpeg ++LOCAL_STATIC_LIBRARIES := libjpeg + + LOCAL_C_INCLUDES := $(LOCAL_PATH) + +@@ -189,7 +188,7 @@ + + LOCAL_SRC_FILES:= $(tjbench_SOURCES) + +-LOCAL_SHARED_LIBRARIES := libjpeg ++LOCAL_STATIC_LIBRARIES := libjpeg + + LOCAL_C_INCLUDES := $(LOCAL_PATH) + +@@ -215,7 +214,7 @@ + + LOCAL_SRC_FILES:= $(rdjpgcom_SOURCES) + +-LOCAL_SHARED_LIBRARIES := libjpeg ++LOCAL_STATIC_LIBRARIES := libjpeg + + LOCAL_C_INCLUDES := $(LOCAL_PATH) + +@@ -240,7 +239,7 @@ + + LOCAL_SRC_FILES:= $(wrjpgcom_SOURCES) + +-LOCAL_SHARED_LIBRARIES := libjpeg ++LOCAL_STATIC_LIBRARIES := libjpeg + + LOCAL_C_INCLUDES := $(LOCAL_PATH) + diff --git a/recipes/jpeg/remove-version.patch b/recipes/jpeg/remove-version.patch new file mode 100644 index 0000000..311aa33 --- /dev/null +++ b/recipes/jpeg/remove-version.patch @@ -0,0 +1,12 @@ +--- jpeg/CMakeLists.txt.orig 2018-11-12 20:20:28.000000000 +0100 ++++ jpeg/CMakeLists.txt 2018-12-14 12:43:45.338704504 +0100 +@@ -573,6 +573,9 @@ + add_library(turbojpeg SHARED ${TURBOJPEG_SOURCES}) + set_property(TARGET turbojpeg PROPERTY COMPILE_FLAGS + "-DBMP_SUPPORTED -DPPM_SUPPORTED") ++ set_property(TARGET jpeg PROPERTY NO_SONAME 1) ++ set_property(TARGET turbojpeg PROPERTY NO_SONAME 1) ++ set(CMAKE_SHARED_LIBRARY_SONAME_C_FLAG "") + if(WIN32) + set_target_properties(turbojpeg PROPERTIES DEFINE_SYMBOL DLLDEFINE) + endif() diff --git a/recipes/numpy/__init__.py b/recipes/numpy/__init__.py deleted file mode 100644 index 55a0279..0000000 --- a/recipes/numpy/__init__.py +++ /dev/null @@ -1,75 +0,0 @@ -from pythonforandroid.recipe import CompiledComponentsPythonRecipe -from pythonforandroid.logger import shprint, info -from pythonforandroid.util import current_directory -from multiprocessing import cpu_count -from os.path import join -import glob -import sh -import shutil - - -class NumpyRecipe(CompiledComponentsPythonRecipe): - - version = '1.22.3' - url = 'https://pypi.python.org/packages/source/n/numpy/numpy-{version}.zip' - site_packages_name = 'numpy' - depends = ['setuptools', 'cython'] - install_in_hostpython = True - call_hostpython_via_targetpython = False - - patches = [ - join("patches", "remove-default-paths.patch"), - join("patches", "add_libm_explicitly_to_build.patch"), - join("patches", "ranlib.patch"), - ] - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super().get_recipe_env(arch, with_flags_in_cc) - - # _PYTHON_HOST_PLATFORM declares that we're cross-compiling - # and avoids issues when building on macOS for Android targets. - env["_PYTHON_HOST_PLATFORM"] = arch.command_prefix - - # NPY_DISABLE_SVML=1 allows numpy to build for non-AVX512 CPUs - # See: https://github.com/numpy/numpy/issues/21196 - env["NPY_DISABLE_SVML"] = "1" - - return env - - def _build_compiled_components(self, arch): - info('Building compiled components in {}'.format(self.name)) - - env = self.get_recipe_env(arch) - with current_directory(self.get_build_dir(arch.arch)): - hostpython = sh.Command(self.hostpython_location) - shprint(hostpython, 'setup.py', self.build_cmd, '-v', - _env=env, *self.setup_extra_args) - build_dir = glob.glob('build/lib.*')[0] - shprint(sh.find, build_dir, '-name', '"*.o"', '-exec', - env['STRIP'], '{}', ';', _env=env) - - 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', '--force', _env=env) - shprint(hostpython, 'setup.py', self.build_cmd, '-v', _env=env, - *self.setup_extra_args) - - def build_compiled_components(self, arch): - self.setup_extra_args = ['-j', str(cpu_count())] - self._build_compiled_components(arch) - self.setup_extra_args = [] - - def rebuild_compiled_components(self, arch, env): - self.setup_extra_args = ['-j', str(cpu_count())] - self._rebuild_compiled_components(arch, env) - self.setup_extra_args = [] - - def get_hostrecipe_env(self, arch): - env = super().get_hostrecipe_env(arch) - env['RANLIB'] = shutil.which('ranlib') - return env - - -recipe = NumpyRecipe() diff --git a/recipes/numpy/patches/add_libm_explicitly_to_build.patch b/recipes/numpy/patches/add_libm_explicitly_to_build.patch deleted file mode 100644 index f9ba9e9..0000000 --- a/recipes/numpy/patches/add_libm_explicitly_to_build.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/numpy/linalg/setup.py b/numpy/linalg/setup.py -index 66c07c9..d34bd93 100644 ---- a/numpy/linalg/setup.py -+++ b/numpy/linalg/setup.py -@@ -46,6 +46,7 @@ def configuration(parent_package='', top_path=None): - sources=['lapack_litemodule.c', get_lapack_lite_sources], - depends=['lapack_lite/f2c.h'], - extra_info=lapack_info, -+ libraries=['m'], - ) - - # umath_linalg module -@@ -54,7 +54,7 @@ def configuration(parent_package='', top_path=None): - sources=['umath_linalg.c.src', get_lapack_lite_sources], - depends=['lapack_lite/f2c.h'], - extra_info=lapack_info, -- libraries=['npymath'], -+ libraries=['npymath', 'm'], - ) - return config diff --git a/recipes/numpy/patches/ranlib.patch b/recipes/numpy/patches/ranlib.patch deleted file mode 100644 index c0b5dad..0000000 --- a/recipes/numpy/patches/ranlib.patch +++ /dev/null @@ -1,11 +0,0 @@ -diff -Naur numpy.orig/numpy/distutils/unixccompiler.py numpy/numpy/distutils/unixccompiler.py ---- numpy.orig/numpy/distutils/unixccompiler.py 2022-05-28 10:22:10.000000000 +0200 -+++ numpy/numpy/distutils/unixccompiler.py 2022-05-28 10:22:24.000000000 +0200 -@@ -124,6 +124,7 @@ - # platform intelligence here to skip ranlib if it's not - # needed -- or maybe Python's configure script took care of - # it for us, hence the check for leading colon. -+ self.ranlib = [os.environ.get('RANLIB')] - if self.ranlib: - display = '%s:@ %s' % (os.path.basename(self.ranlib[0]), - output_filename) diff --git a/recipes/numpy/patches/remove-default-paths.patch b/recipes/numpy/patches/remove-default-paths.patch deleted file mode 100644 index 3581f0f..0000000 --- a/recipes/numpy/patches/remove-default-paths.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git a/numpy/distutils/system_info.py b/numpy/distutils/system_info.py -index fc7018a..7b514bc 100644 ---- a/numpy/distutils/system_info.py -+++ b/numpy/distutils/system_info.py -@@ -340,10 +340,10 @@ if os.path.join(sys.prefix, 'lib') not in default_lib_dirs: - default_include_dirs.append(os.path.join(sys.prefix, 'include')) - default_src_dirs.append(os.path.join(sys.prefix, 'src')) - --default_lib_dirs = [_m for _m in default_lib_dirs if os.path.isdir(_m)] --default_runtime_dirs = [_m for _m in default_runtime_dirs if os.path.isdir(_m)] --default_include_dirs = [_m for _m in default_include_dirs if os.path.isdir(_m)] --default_src_dirs = [_m for _m in default_src_dirs if os.path.isdir(_m)] -+default_lib_dirs = [] #[_m for _m in default_lib_dirs if os.path.isdir(_m)] -+default_runtime_dirs =[] # [_m for _m in default_runtime_dirs if os.path.isdir(_m)] -+default_include_dirs =[] # [_m for _m in default_include_dirs if os.path.isdir(_m)] -+default_src_dirs =[] # [_m for _m in default_src_dirs if os.path.isdir(_m)] - - so_ext = get_shared_lib_extension() - -@@ -814,7 +814,7 @@ class system_info(object): - path = self.get_paths(self.section, key) - if path == ['']: - path = [] -- return path -+ return [] - - def get_include_dirs(self, key='include_dirs'): - return self.get_paths(self.section, key) diff --git a/recipes/pycodec2/__init__.py b/recipes/pycodec2/__init__.py index 6ad05bd..5b71a80 100644 --- a/recipes/pycodec2/__init__.py +++ b/recipes/pycodec2/__init__.py @@ -5,7 +5,7 @@ import sh # class PyCodec2Recipe(IncludedFilesBehaviour, CythonRecipe): class PyCodec2Recipe(CythonRecipe): - url = "https://github.com/markqvist/pycodec2/archive/refs/heads/main.zip" + url = "https://github.com/markqvist/pycodec2/archive/438ee4f2f3ee30635a34caddf520cfaccdbbc646.zip" # src_filename = "../../../pycodec2" depends = ["setuptools", "numpy", "Cython", "codec2"] call_hostpython_via_targetpython = False diff --git a/recipes/pyjnius/__init__.py b/recipes/pyjnius/__init__.py new file mode 100644 index 0000000..86d8803 --- /dev/null +++ b/recipes/pyjnius/__init__.py @@ -0,0 +1,44 @@ +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(PyProjectRecipe): + version = '1.7.0' + url = 'https://github.com/kivy/pyjnius/archive/{version}.zip' + name = 'pyjnius' + depends = [('genericndkbuild', 'sdl2', 'sdl3'), 'six'] + site_packages_name = 'jnius' + hostpython_prerequisites = ["Cython<3.2"] + patches = [ + "use_cython.patch", + ('genericndkbuild_jnienv_getter.patch', will_build('genericndkbuild')), + ('sdl3_jnienv_getter.patch', will_build('sdl3')), + ] + + def get_recipe_env(self, arch, **kwargs): + env = super().get_recipe_env(arch, **kwargs) + + # Taken from CythonRecipe + env['LDFLAGS'] = env['LDFLAGS'] + ' -L{} '.format( + self.ctx.get_libs_dir(arch.arch) + + ' -L{} '.format(self.ctx.libs_dir) + + ' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'obj', 'local', + arch.arch))) + env['LDSHARED'] = env['CC'] + ' -shared' + env['LIBLINK'] = 'NOTNONE' + + # NDKPLATFORM is our switch for detecting Android platform, so can't be None + env['NDKPLATFORM'] = "NOTNONE" + return env + + def postbuild_arch(self, arch): + super().postbuild_arch(arch) + info('Copying pyjnius java class to classes build dir') + with current_directory(self.get_build_dir(arch.arch)): + shprint(sh.cp, '-a', join('jnius', 'src', 'org'), self.ctx.javaclass_dir) + + +recipe = PyjniusRecipe() diff --git a/recipes/pyjnius/genericndkbuild_jnienv_getter.patch b/recipes/pyjnius/genericndkbuild_jnienv_getter.patch new file mode 100644 index 0000000..fcd5387 --- /dev/null +++ b/recipes/pyjnius/genericndkbuild_jnienv_getter.patch @@ -0,0 +1,24 @@ +diff -Naur pyjnius.orig/jnius/env.py pyjnius/jnius/env.py +--- pyjnius.orig/jnius/env.py 2022-05-28 11:16:02.000000000 +0200 ++++ pyjnius/jnius/env.py 2022-05-28 11:18:30.000000000 +0200 +@@ -268,7 +268,7 @@ + + class AndroidJavaLocation(UnixJavaLocation): + def get_libraries(self): +- return ['SDL2', 'log'] ++ return ['main', 'log'] + + def get_include_dirs(self): + # When cross-compiling for Android, we should not use the include dirs +diff -Naur pyjnius.orig/jnius/jnius_jvm_android.pxi pyjnius/jnius/jnius_jvm_android.pxi +--- pyjnius.orig/jnius/jnius_jvm_android.pxi 2022-05-28 11:16:02.000000000 +0200 ++++ pyjnius/jnius/jnius_jvm_android.pxi 2022-05-28 11:17:17.000000000 +0200 +@@ -1,6 +1,6 @@ + # on android, rely on SDL to get the JNI env +-cdef extern JNIEnv *SDL_AndroidGetJNIEnv() ++cdef extern JNIEnv *WebView_AndroidGetJNIEnv() + + + cdef JNIEnv *get_platform_jnienv() except NULL: +- return SDL_AndroidGetJNIEnv() ++ return WebView_AndroidGetJNIEnv() diff --git a/recipes/pyjnius/sdl3_jnienv_getter.patch b/recipes/pyjnius/sdl3_jnienv_getter.patch new file mode 100644 index 0000000..d91da76 --- /dev/null +++ b/recipes/pyjnius/sdl3_jnienv_getter.patch @@ -0,0 +1,24 @@ +diff -Naur pyjnius.orig/jnius/env.py pyjnius/jnius/env.py +--- pyjnius.orig/jnius/env.py 2022-05-28 11:16:02.000000000 +0200 ++++ pyjnius/jnius/env.py 2022-05-28 11:18:30.000000000 +0200 +@@ -268,7 +268,7 @@ + + class AndroidJavaLocation(UnixJavaLocation): + def get_libraries(self): +- return ['SDL2', 'log'] ++ return ['SDL3', 'log'] + + def get_include_dirs(self): + # When cross-compiling for Android, we should not use the include dirs +diff -Naur pyjnius.orig/jnius/jnius_jvm_android.pxi pyjnius/jnius/jnius_jvm_android.pxi +--- pyjnius.orig/jnius/jnius_jvm_android.pxi 2022-05-28 11:16:02.000000000 +0200 ++++ pyjnius/jnius/jnius_jvm_android.pxi 2022-05-28 11:17:17.000000000 +0200 +@@ -1,6 +1,6 @@ + # on android, rely on SDL to get the JNI env +-cdef extern JNIEnv *SDL_AndroidGetJNIEnv() ++cdef extern JNIEnv *SDL_GetAndroidJNIEnv() + + + cdef JNIEnv *get_platform_jnienv() except NULL: +- return SDL_AndroidGetJNIEnv() ++ return SDL_GetAndroidJNIEnv() diff --git a/recipes/pyjnius/use_cython.patch b/recipes/pyjnius/use_cython.patch new file mode 100644 index 0000000..59265e9 --- /dev/null +++ b/recipes/pyjnius/use_cython.patch @@ -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" diff --git a/recipes/python3/__init__.py b/recipes/python3/__init__.py new file mode 100644 index 0000000..2a2e07d --- /dev/null +++ b/recipes/python3/__init__.py @@ -0,0 +1,445 @@ +import glob +import sh +import subprocess + +from os import environ, utime +from os.path import dirname, exists, join +from pathlib import Path +import shutil + +from pythonforandroid.logger import info, warning, shprint +from pythonforandroid.patching import version_starts_with +from pythonforandroid.recipe import Recipe, TargetPythonRecipe +from pythonforandroid.util import ( + current_directory, + ensure_dir, + walk_valid_filens, + BuildInterruptingException, +) + +NDK_API_LOWER_THAN_SUPPORTED_MESSAGE = ( + 'Target ndk-api is {ndk_api}, ' + 'but the python3 recipe supports only {min_ndk_api}+' +) + + +class Python3Recipe(TargetPythonRecipe): + ''' + The python3's recipe + ^^^^^^^^^^^^^^^^^^^^ + + The python 3 recipe can be built with some extra python modules, but to do + so, we need some libraries. By default, we ship the python3 recipe with + some common libraries, defined in ``depends``. We also support some optional + libraries, which are less common that the ones defined in ``depends``, so + we added them as optional dependencies (``opt_depends``). + + Below you have a relationship between the python modules and the recipe + libraries:: + + - _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). + + .. 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' + name = 'python3' + + patches = [ + 'patches/pyconfig_detection.patch', + 'patches/reproducible-buildinfo.diff', + + # 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")), + + # 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 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 = ( + '--host={android_host}', + '--build={android_build}', + '--enable-shared', + '--enable-ipv6', + 'ac_cv_file__dev_ptmx=yes', + 'ac_cv_file__dev_ptc=no', + '--without-ensurepip', + 'ac_cv_little_endian_double=yes', + 'ac_cv_header_sys_eventfd_h=no', + '--prefix={prefix}', + '--exec-prefix={exec_prefix}', + '--enable-loadable-sqlite-extensions' + ) + + if version_starts_with("3.11"): + configure_args += ('--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 + recipe does). + ''' + + MIN_NDK_API = 21 + '''Sets the minimal ndk api number needed to use the recipe. + + .. warning:: This recipe can be built only against API 21+, so it means + that any class which inherits from class:`GuestPythonRecipe` will have + this limitation. + ''' + + stdlib_dir_blacklist = { + '__pycache__', + 'test', + 'tests', + 'lib2to3', + 'ensurepip', + 'idlelib', + 'tkinter', + } + '''The directories that we want to omit for our python bundle''' + + stdlib_filen_blacklist = [ + '*.py', + '*.exe', + '*.whl', + ] + '''The file extensions that we want to blacklist for our python bundle''' + + site_packages_dir_blacklist = { + '__pycache__', + 'tests' + } + '''The directories from site packages dir that we don't want to be included + in our python bundle.''' + + site_packages_filen_blacklist = [ + '*.py' + ] + '''The file extensions from site packages dir that we don't want to be + included in our python bundle.''' + + compiled_extension = '.pyc' + '''the default extension for compiled python files. + + .. note:: the default extension for compiled python files has been .pyo for + python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no + longer used and has been removed in favour of extension .pyc + ''' + + def __init__(self, *args, **kwargs): + self._ctx = None + super().__init__(*args, **kwargs) + + @property + def _libpython(self): + '''return the python's library name (with extension)''' + return 'libpython{link_version}.so'.format( + link_version=self.link_version + ) + + @property + def link_version(self): + '''return the python's library link version e.g. 3.7m, 3.8''' + major, minor = self.major_minor_version_string.split('.') + flags = '' + if major == '3' and int(minor) < 8: + flags += 'm' + return '{major}.{minor}{flags}'.format( + major=major, + minor=minor, + flags=flags + ) + + def include_root(self, arch_name): + return join(self.get_build_dir(arch_name), 'Include') + + def link_root(self, arch_name): + return join(self.get_build_dir(arch_name), 'android-build') + + def should_build(self, arch): + return not Path(self.link_root(arch.arch), self._libpython).is_file() + + def prebuild_arch(self, arch): + super().prebuild_arch(arch) + self.ctx.python_recipe = self + + def get_recipe_env(self, arch=None, with_flags_in_cc=True): + env = super().get_recipe_env(arch) + env['HOSTARCH'] = arch.command_prefix + + env['CC'] = arch.get_clang_exe(with_target=True) + + env['PATH'] = ( + '{hostpython_dir}:{old_path}').format( + hostpython_dir=self.get_recipe( + 'host' + self.name, self.ctx).get_path_to_python(), + old_path=env['PATH']) + + env['CFLAGS'] = ' '.join( + [ + '-fPIC', + '-DANDROID' + ] + ) + + 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.') + + return env + + def set_libs_flags(self, env, arch): + '''Takes care to properly link libraries with python depending on our + requirements and the attribute :attr:`opt_depends`. + ''' + 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()) + + 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()) + + # python build system contains hardcoded zlib version which prevents + # the build of zlib module, here we search for android's zlib version + # and sets the right flags, so python can be build with android's zlib + info("Activating flags for android's zlib") + zlib_lib_path = arch.ndk_lib_dir_versioned + zlib_includes = self.ctx.ndk.sysroot_include_dir + zlib_h = join(zlib_includes, 'zlib.h') + try: + with open(zlib_h) as fileh: + zlib_data = fileh.read() + except IOError: + raise BuildInterruptingException( + "Could not determine android's zlib version, no zlib.h ({}) in" + " the NDK dir includes".format(zlib_h) + ) + for line in zlib_data.split('\n'): + if line.startswith('#define ZLIB_VERSION '): + break + else: + raise BuildInterruptingException( + 'Could not parse zlib.h...so we cannot find zlib version,' + 'required by python build,' + ) + env['ZLIB_VERSION'] = line.replace('#define ZLIB_VERSION ', '') + add_flags(' -I' + zlib_includes, ' -L' + zlib_lib_path, ' -lz') + + return env + + def build_arch(self, arch): + if self.ctx.ndk_api < self.MIN_NDK_API: + raise BuildInterruptingException( + NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format( + ndk_api=self.ctx.ndk_api, min_ndk_api=self.MIN_NDK_API + ), + ) + + recipe_build_dir = self.get_build_dir(arch.arch) + + # Create a subdirectory to actually perform the build + build_dir = join(recipe_build_dir, 'android-build') + ensure_dir(build_dir) + + # TODO: Get these dynamically, like bpo-30386 does + 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() + + with current_directory(build_dir): + if not exists('config.status'): + shprint( + sh.Command(join(recipe_build_dir, 'configure')), + *(' '.join(self.configure_args).format( + android_host=env['HOSTARCH'], + android_build=android_build, + python_host_bin=join(self.get_recipe( + 'host' + self.name, self.ctx + ).get_path_to_python(), "python3"), + prefix=sys_prefix, + 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, + 'all', + 'INSTSONAME={lib_name}'.format(lib_name=self._libpython), + _env=env + ) + + # TODO: Look into passing the path to pyconfig.h in a + # better way, although this is probably acceptable + sh.cp('pyconfig.h', join(recipe_build_dir, 'Include')) + + def compile_python_files(self, dir): + ''' + Compile the python files (recursively) for the python files inside + a given folder. + + .. note:: python2 compiles the files into extension .pyo, but in + python3, and as of Python 3.5, the .pyo filename extension is no + longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488) + ''' + args = [self.ctx.hostpython] + args += ['-OO', '-m', 'compileall', '-b', '-f', dir] + subprocess.call(args) + + def create_python_bundle(self, dirn, arch): + """ + Create a packaged python bundle in the target directory, by + copying all the modules and standard library to the right + place. + """ + # Todo: find a better way to find the build libs folder + modules_build_dir = join( + self.get_build_dir(arch.arch), + 'android-build', + 'build', + 'lib.linux{}-{}-{}'.format( + '2' if self.version[0] == '2' else '', + arch.command_prefix.split('-')[0], + self.major_minor_version_string + )) + + # Compile to *.pyc the python modules + self.compile_python_files(modules_build_dir) + # Compile to *.pyc the standard python library + self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib')) + # Compile to *.pyc the other python packages (site-packages) + self.compile_python_files(self.ctx.get_python_install_dir(arch.arch)) + + # Bundle compiled python modules to a folder + modules_dir = join(dirn, 'modules') + c_ext = self.compiled_extension + ensure_dir(modules_dir) + module_filens = (glob.glob(join(modules_build_dir, '*.so')) + + glob.glob(join(modules_build_dir, '*' + c_ext))) + info("Copy {} files into the bundle".format(len(module_filens))) + for filen in module_filens: + info(" - copy {}".format(filen)) + shutil.copy2(filen, modules_dir) + + # zip up the standard library + stdlib_zip = join(dirn, 'stdlib.zip') + with current_directory(join(self.get_build_dir(arch.arch), 'Lib')): + stdlib_filens = list(walk_valid_filens( + '.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist)) + if 'SOURCE_DATE_EPOCH' in environ: + # for reproducible builds + stdlib_filens.sort() + timestamp = int(environ['SOURCE_DATE_EPOCH']) + for filen in stdlib_filens: + utime(filen, (timestamp, timestamp)) + info("Zip {} files into the bundle".format(len(stdlib_filens))) + shprint(sh.zip, '-X', stdlib_zip, *stdlib_filens) + + # copy the site-packages into place + ensure_dir(join(dirn, 'site-packages')) + ensure_dir(self.ctx.get_python_install_dir(arch.arch)) + # TODO: Improve the API around walking and copying the files + with current_directory(self.ctx.get_python_install_dir(arch.arch)): + filens = list(walk_valid_filens( + '.', self.site_packages_dir_blacklist, + self.site_packages_filen_blacklist)) + info("Copy {} files into the site-packages".format(len(filens))) + for filen in filens: + info(" - copy {}".format(filen)) + ensure_dir(join(dirn, 'site-packages', dirname(filen))) + shutil.copy2(filen, join(dirn, 'site-packages', filen)) + + # copy the python .so files into place + python_build_dir = join(self.get_build_dir(arch.arch), + 'android-build') + python_lib_name = 'libpython' + self.link_version + shprint( + sh.cp, + join(python_build_dir, python_lib_name + '.so'), + join(self.ctx.bootstrap.dist_dir, 'libs', arch.arch) + ) + + info('Renaming .so files to reflect cross-compile') + self.reduce_object_file_names(join(dirn, 'site-packages')) + + return join(dirn, 'site-packages') + + +recipe = Python3Recipe() \ No newline at end of file diff --git a/recipes/python3/patches/cpython-311-ctypes-find-library.patch b/recipes/python3/patches/cpython-311-ctypes-find-library.patch new file mode 100644 index 0000000..7864d57 --- /dev/null +++ b/recipes/python3/patches/cpython-311-ctypes-find-library.patch @@ -0,0 +1,19 @@ +--- Python-3.11.5/Lib/ctypes/util.py 2023-08-24 17:39:18.000000000 +0530 ++++ Python-3.11.5.mod/Lib/ctypes/util.py 2023-11-18 22:12:17.356160615 +0530 +@@ -4,7 +4,15 @@ + import sys + + # find_library(name) returns the pathname of a library, or None. +-if os.name == "nt": ++ ++# This patch overrides the find_library to look in the right places on ++# Android ++if True: ++ from android._ctypes_library_finder import find_library as _find_lib ++ def find_library(name): ++ return _find_lib(name) ++ ++elif os.name == "nt": + + def _get_build_version(): + """Return the version of MSVC that was used to build Python. diff --git a/recipes/python3/patches/py3.7.1_fix-ctypes-util-find-library.patch b/recipes/python3/patches/py3.7.1_fix-ctypes-util-find-library.patch new file mode 100644 index 0000000..494270d --- /dev/null +++ b/recipes/python3/patches/py3.7.1_fix-ctypes-util-find-library.patch @@ -0,0 +1,15 @@ +diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py +--- a/Lib/ctypes/util.py ++++ b/Lib/ctypes/util.py +@@ -67,4 +67,11 @@ + return fname + return None + ++# This patch overrides the find_library to look in the right places on ++# Android ++if True: ++ from android._ctypes_library_finder import find_library as _find_lib ++ def find_library(name): ++ return _find_lib(name) ++ + elif os.name == "posix" and sys.platform == "darwin": diff --git a/recipes/python3/patches/py3.7.1_fix-zlib-version.patch b/recipes/python3/patches/py3.7.1_fix-zlib-version.patch new file mode 100644 index 0000000..0dbffae --- /dev/null +++ b/recipes/python3/patches/py3.7.1_fix-zlib-version.patch @@ -0,0 +1,12 @@ +--- Python-3.7.1/setup.py.orig 2018-10-20 08:04:19.000000000 +0200 ++++ Python-3.7.1/setup.py 2019-02-17 00:24:30.715904412 +0100 +@@ -1410,7 +1410,8 @@ class PyBuildExt(build_ext): + if zlib_inc is not None: + zlib_h = zlib_inc[0] + '/zlib.h' + version = '"0.0.0"' +- version_req = '"1.1.3"' ++ version_req = '"{}"'.format( ++ os.environ.get('ZLIB_VERSION', '1.1.3')) + if host_platform == 'darwin' and is_macosx_sdk_path(zlib_h): + zlib_h = os.path.join(macosx_sdk_root(), zlib_h[1:]) + with open(zlib_h) as fp: diff --git a/recipes/python3/patches/py3.7.1_fix_cortex_a8.patch b/recipes/python3/patches/py3.7.1_fix_cortex_a8.patch new file mode 100644 index 0000000..5ddc3c4 --- /dev/null +++ b/recipes/python3/patches/py3.7.1_fix_cortex_a8.patch @@ -0,0 +1,14 @@ +This patch removes --fix-cortex-a8 from the linker flags in order to support linking +with lld, as lld does not support this flag (https://github.com/android-ndk/ndk/issues/766). +diff --git a/configure b/configure +--- a/configure ++++ b/configure +@@ -5671,7 +5671,7 @@ $as_echo_n "checking for the Android arm ABI... " >&6; } + $as_echo "$_arm_arch" >&6; } + if test "$_arm_arch" = 7; then + BASECFLAGS="${BASECFLAGS} -mfloat-abi=softfp -mfpu=vfpv3-d16" +- LDFLAGS="${LDFLAGS} -march=armv7-a -Wl,--fix-cortex-a8" ++ LDFLAGS="${LDFLAGS} -march=armv7-a" + fi + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: not Android" >&5 \ No newline at end of file diff --git a/recipes/python3/patches/py3.8.1.patch b/recipes/python3/patches/py3.8.1.patch new file mode 100644 index 0000000..6018805 --- /dev/null +++ b/recipes/python3/patches/py3.8.1.patch @@ -0,0 +1,42 @@ +diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py +index 97973bc..053c231 100644 +--- a/Lib/ctypes/util.py ++++ b/Lib/ctypes/util.py +@@ -67,6 +67,13 @@ if os.name == "nt": + return fname + return None + ++# This patch overrides the find_library to look in the right places on ++# Android ++if True: ++ from android._ctypes_library_finder import find_library as _find_lib ++ def find_library(name): ++ return _find_lib(name) ++ + elif os.name == "posix" and sys.platform == "darwin": + from ctypes.macholib.dyld import dyld_find as _dyld_find + def find_library(name): +diff --git a/configure b/configure +index 0914e24..dd00812 100755 +--- a/configure ++++ b/configure +@@ -18673,4 +18673,3 @@ if test "$Py_OPT" = 'false' -a "$Py_DEBUG" != 'true'; then + echo "" >&6 + echo "" >&6 + fi +- +diff --git a/setup.py b/setup.py +index 20d7f35..af15cc2 100644 +--- a/setup.py ++++ b/setup.py +@@ -1501,7 +1501,9 @@ class PyBuildExt(build_ext): + if zlib_inc is not None: + zlib_h = zlib_inc[0] + '/zlib.h' + version = '"0.0.0"' +- version_req = '"1.1.3"' ++ # version_req = '"1.1.3"' ++ version_req = '"{}"'.format( ++ os.environ.get('ZLIB_VERSION', '1.1.3')) + if MACOS and is_macosx_sdk_path(zlib_h): + zlib_h = os.path.join(macosx_sdk_root(), zlib_h[1:]) + with open(zlib_h) as fp: diff --git a/recipes/python3/patches/py3.8.1_fix_cortex_a8.patch b/recipes/python3/patches/py3.8.1_fix_cortex_a8.patch new file mode 100644 index 0000000..92a41b5 --- /dev/null +++ b/recipes/python3/patches/py3.8.1_fix_cortex_a8.patch @@ -0,0 +1,15 @@ +This patch removes --fix-cortex-a8 from the linker flags in order to support linking +with lld, as lld does not support this flag (https://github.com/android-ndk/ndk/issues/766). +diff --git a/configure b/configure +index 0914e24..7517168 100755 +--- a/configure ++++ b/configure +@@ -5642,7 +5642,7 @@ $as_echo_n "checking for the Android arm ABI... " >&6; } + $as_echo "$_arm_arch" >&6; } + if test "$_arm_arch" = 7; then + BASECFLAGS="${BASECFLAGS} -mfloat-abi=softfp -mfpu=vfpv3-d16" +- LDFLAGS="${LDFLAGS} -march=armv7-a -Wl,--fix-cortex-a8" ++ LDFLAGS="${LDFLAGS} -march=armv7-a" + fi + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: not Android" >&5 diff --git a/recipes/python3/patches/pyconfig_detection.patch b/recipes/python3/patches/pyconfig_detection.patch new file mode 100644 index 0000000..087ab58 --- /dev/null +++ b/recipes/python3/patches/pyconfig_detection.patch @@ -0,0 +1,13 @@ +diff -Nru Python-3.8.2/Lib/site.py Python-3.8.2-new/Lib/site.py +--- Python-3.8.2/Lib/site.py 2020-04-28 12:48:38.000000000 -0700 ++++ Python-3.8.2-new/Lib/site.py 2020-04-28 12:52:46.000000000 -0700 +@@ -487,7 +487,8 @@ + if key == 'include-system-site-packages': + system_site = value.lower() + elif key == 'home': +- sys._home = value ++ # this is breaking pyconfig.h path detection with venv ++ print('Ignoring "sys._home = value" override') + + sys.prefix = sys.exec_prefix = site_prefix + diff --git a/recipes/python3/patches/reproducible-buildinfo.diff b/recipes/python3/patches/reproducible-buildinfo.diff new file mode 100644 index 0000000..807d180 --- /dev/null +++ b/recipes/python3/patches/reproducible-buildinfo.diff @@ -0,0 +1,13 @@ +# DP: Build getbuildinfo.o with DATE/TIME values when defined + +--- a/Makefile.pre.in ++++ b/Makefile.pre.in +@@ -785,6 +785,8 @@ Modules/getbuildinfo.o: $(PARSER_OBJS) \ + -DGITVERSION="\"`LC_ALL=C $(GITVERSION)`\"" \ + -DGITTAG="\"`LC_ALL=C $(GITTAG)`\"" \ + -DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \ ++ $(if $(BUILD_DATE),-DDATE='"$(BUILD_DATE)"') \ ++ $(if $(BUILD_TIME),-DTIME='"$(BUILD_TIME)"') \ + -o $@ $(srcdir)/Modules/getbuildinfo.c + + Modules/getpath.o: $(srcdir)/Modules/getpath.c Makefile diff --git a/recipes/sqlite3/Android.mk b/recipes/sqlite3/Android.mk new file mode 100644 index 0000000..05d13cd --- /dev/null +++ b/recipes/sqlite3/Android.mk @@ -0,0 +1,11 @@ +LOCAL_PATH := $(call my-dir)/.. + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := sqlite3.c + +LOCAL_MODULE := sqlite3 + +LOCAL_CFLAGS := -DSQLITE_ENABLE_FTS4 -D_FILE_OFFSET_BITS=32 -DSQLITE_ENABLE_JSON1 + +include $(BUILD_SHARED_LIBRARY) \ No newline at end of file diff --git a/recipes/sqlite3/__init__.py b/recipes/sqlite3/__init__.py new file mode 100644 index 0000000..2970302 --- /dev/null +++ b/recipes/sqlite3/__init__.py @@ -0,0 +1,36 @@ +from os.path import join +import shutil + +from pythonforandroid.recipe import NDKRecipe +from pythonforandroid.util import ensure_dir + + +class Sqlite3Recipe(NDKRecipe): + version = '3.35.5' + # Don't forget to change the URL when changing the version + url = 'https://www.sqlite.org/2021/sqlite-amalgamation-3350500.zip' + generated_libraries = ['sqlite3'] + + def should_build(self, arch): + return not self.has_libs(arch, 'libsqlite3.so') + + def prebuild_arch(self, arch): + super().prebuild_arch(arch) + # Copy the Android make file + ensure_dir(join(self.get_build_dir(arch.arch), 'jni')) + shutil.copyfile(join(self.get_recipe_dir(), 'Android.mk'), + join(self.get_build_dir(arch.arch), 'jni/Android.mk')) + + def build_arch(self, arch, *extra_args): + super().build_arch(arch) + # Copy the shared library + shutil.copyfile(join(self.get_build_dir(arch.arch), 'libs', arch.arch, 'libsqlite3.so'), + join(self.ctx.get_libs_dir(arch.arch), 'libsqlite3.so')) + + def get_recipe_env(self, arch): + env = super().get_recipe_env(arch) + env['NDK_PROJECT_PATH'] = self.get_build_dir(arch.arch) + return env + + +recipe = Sqlite3Recipe() \ No newline at end of file