from os.path import ( abspath, join, realpath, dirname, expanduser, exists, split, isdir ) from os import environ import copy import os import glob import sys import re import sh import shutil import subprocess from contextlib import suppress from pythonforandroid.util import ( current_directory, ensure_dir, BuildInterruptingException, ) from pythonforandroid.logger import (info, warning, info_notify, info_main, shprint) from pythonforandroid.archs import ArchARM, ArchARMv7_a, ArchAarch_64, Archx86, Archx86_64 from pythonforandroid.pythonpackage import get_package_name from pythonforandroid.recipe import CythonRecipe, Recipe from pythonforandroid.recommendations import ( check_ndk_version, check_target_api, check_ndk_api, RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API) def get_ndk_platform_dir(ndk_dir, ndk_api, arch): ndk_platform_dir_exists = True platform_dir = arch.platform_dir ndk_platform = join( ndk_dir, 'platforms', 'android-{}'.format(ndk_api), platform_dir) if not exists(ndk_platform): warning("ndk_platform doesn't exist: {}".format(ndk_platform)) ndk_platform_dir_exists = False return ndk_platform, ndk_platform_dir_exists def get_toolchain_versions(ndk_dir, arch): toolchain_versions = [] toolchain_path_exists = True toolchain_prefix = arch.toolchain_prefix toolchain_path = join(ndk_dir, 'toolchains') if isdir(toolchain_path): toolchain_contents = glob.glob('{}/{}-*'.format(toolchain_path, toolchain_prefix)) toolchain_versions = [split(path)[-1][len(toolchain_prefix) + 1:] for path in toolchain_contents] else: warning('Could not find toolchain subdirectory!') toolchain_path_exists = False return toolchain_versions, toolchain_path_exists def get_targets(sdk_dir): if exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')): avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager')) targets = avdmanager('list', 'target').stdout.decode('utf-8').split('\n') elif exists(join(sdk_dir, 'tools', 'android')): android = sh.Command(join(sdk_dir, 'tools', 'android')) targets = android('list').stdout.decode('utf-8').split('\n') else: raise BuildInterruptingException( 'Could not find `android` or `sdkmanager` binaries in Android SDK', instructions='Make sure the path to the Android SDK is correct') return targets def get_available_apis(sdk_dir): targets = get_targets(sdk_dir) apis = [s for s in targets if re.match(r'^ *API level: ', s)] apis = [re.findall(r'[0-9]+', s) for s in apis] apis = [int(s[0]) for s in apis if s] return apis class Context: '''A build context. If anything will be built, an instance this class will be instantiated and used to hold all the build state.''' # Whether to make a debug or release build build_as_debuggable = False # Whether to strip debug symbols in `.so` files with_debug_symbols = False env = environ.copy() # the filepath of toolchain.py root_dir = None # the root dir where builds and dists will be stored storage_dir = None # in which bootstraps are copied for building # and recipes are built build_dir = None distribution = None """The Distribution object representing the current build target location.""" # the Android project folder where everything ends up dist_dir = None # where Android libs are cached after build # but before being placed in dists libs_dir = None aars_dir = None # Whether setup.py or similar should be used if present: use_setup_py = False ccache = None # whether to use ccache ndk_platform = None # the ndk platform directory bootstrap = None bootstrap_build_dir = None recipe_build_order = None # Will hold the list of all built recipes symlink_bootstrap_files = False # If True, will symlink instead of copying during build java_build_tool = 'auto' @property def packages_path(self): '''Where packages are downloaded before being unpacked''' return join(self.storage_dir, 'packages') @property def templates_dir(self): return join(self.root_dir, 'templates') @property def libs_dir(self): # Was previously hardcoded as self.build_dir/libs directory = join(self.build_dir, 'libs_collections', self.bootstrap.distribution.name) ensure_dir(directory) return directory @property def javaclass_dir(self): # Was previously hardcoded as self.build_dir/java directory = join(self.build_dir, 'javaclasses', self.bootstrap.distribution.name) ensure_dir(directory) return directory @property def aars_dir(self): directory = join(self.build_dir, 'aars', self.bootstrap.distribution.name) ensure_dir(directory) return directory @property def python_installs_dir(self): directory = join(self.build_dir, 'python-installs') ensure_dir(directory) return directory def get_python_install_dir(self): return join(self.python_installs_dir, self.bootstrap.distribution.name) def setup_dirs(self, storage_dir): '''Calculates all the storage and build dirs, and makes sure the directories exist where necessary.''' self.storage_dir = expanduser(storage_dir) if ' ' in self.storage_dir: raise ValueError('storage dir path cannot contain spaces, please ' 'specify a path with --storage-dir') self.build_dir = join(self.storage_dir, 'build') self.dist_dir = join(self.storage_dir, 'dists') def ensure_dirs(self): ensure_dir(self.storage_dir) ensure_dir(self.build_dir) ensure_dir(self.dist_dir) ensure_dir(join(self.build_dir, 'bootstrap_builds')) ensure_dir(join(self.build_dir, 'other_builds')) @property def android_api(self): '''The Android API being targeted.''' if self._android_api is None: raise ValueError('Tried to access android_api but it has not ' 'been set - this should not happen, something ' 'went wrong!') return self._android_api @android_api.setter def android_api(self, value): self._android_api = value @property def ndk_api(self): '''The API number compile against''' if self._ndk_api is None: raise ValueError('Tried to access ndk_api but it has not ' 'been set - this should not happen, something ' 'went wrong!') return self._ndk_api @ndk_api.setter def ndk_api(self, value): self._ndk_api = value @property def sdk_dir(self): '''The path to the Android SDK.''' if self._sdk_dir is None: raise ValueError('Tried to access sdk_dir but it has not ' 'been set - this should not happen, something ' 'went wrong!') return self._sdk_dir @sdk_dir.setter def sdk_dir(self, value): self._sdk_dir = value @property def ndk_dir(self): '''The path to the Android NDK.''' if self._ndk_dir is None: raise ValueError('Tried to access ndk_dir but it has not ' 'been set - this should not happen, something ' 'went wrong!') return self._ndk_dir @ndk_dir.setter def ndk_dir(self, value): self._ndk_dir = value def prepare_build_environment(self, user_sdk_dir, user_ndk_dir, user_android_api, user_ndk_api): '''Checks that build dependencies exist and sets internal variables for the Android SDK etc. ..warning:: This *must* be called before trying any build stuff ''' self.ensure_dirs() if self._build_env_prepared: return ok = True # Work out where the Android SDK is sdk_dir = None if user_sdk_dir: sdk_dir = user_sdk_dir # This is the old P4A-specific var if sdk_dir is None: sdk_dir = environ.get('ANDROIDSDK', None) # This seems used more conventionally if sdk_dir is None: sdk_dir = environ.get('ANDROID_HOME', None) # Checks in the buildozer SDK dir, useful for debug tests of p4a if sdk_dir is None: possible_dirs = glob.glob(expanduser(join( '~', '.buildozer', 'android', 'platform', 'android-sdk-*'))) possible_dirs = [d for d in possible_dirs if not d.endswith(('.bz2', '.gz'))] if possible_dirs: info('Found possible SDK dirs in buildozer dir: {}'.format( ', '.join(d.split(os.sep)[-1] for d in possible_dirs))) info('Will attempt to use SDK at {}'.format(possible_dirs[0])) warning('This SDK lookup is intended for debug only, if you ' 'use python-for-android much you should probably ' 'maintain your own SDK download.') sdk_dir = possible_dirs[0] if sdk_dir is None: raise BuildInterruptingException('Android SDK dir was not specified, exiting.') self.sdk_dir = realpath(sdk_dir) # Check what Android API we're using android_api = None if user_android_api: android_api = user_android_api info('Getting Android API version from user argument: {}'.format(android_api)) elif 'ANDROIDAPI' in environ: android_api = environ['ANDROIDAPI'] info('Found Android API target in $ANDROIDAPI: {}'.format(android_api)) else: