def prebuild_arch(self, arch): super(TargetPythonRecipe, self).prebuild_arch(arch) if self.from_crystax and self.ctx.ndk != 'crystax': error('The {} recipe can only be built when ' 'using the CrystaX NDK. Exiting.'.format(self.name)) exit(1) self.ctx.python_recipe = self
def prebuild_arch(self, arch): super(AndroidRecipe, self).prebuild_arch(arch) ctx_bootstrap = self.ctx.bootstrap.name # define macros for Cython, C, Python tpxi = 'DEF {} = {}\n' th = '#define {} {}\n' tpy = '{} = {}\n' # make sure bootstrap name is in unicode if isinstance(ctx_bootstrap, bytes): ctx_bootstrap = ctx_bootstrap.decode('utf-8') bootstrap = bootstrap_name = ctx_bootstrap is_sdl2 = bootstrap_name in ('sdl2', 'sdl2python3', 'sdl2_gradle') is_webview = bootstrap_name in ('webview',) if is_sdl2 or is_webview: if is_sdl2: bootstrap = 'sdl2' java_ns = u'org.kivy.android' jni_ns = u'org/kivy/android' else: logger.error(( 'unsupported bootstrap for android recipe: {}' ''.format(bootstrap_name) )) exit(1) config = { 'BOOTSTRAP': bootstrap, 'IS_SDL2': int(is_sdl2), 'PY2': int(will_build('python2')(self)), 'JAVA_NAMESPACE': java_ns, 'JNI_NAMESPACE': jni_ns, } # create config files for Cython, C and Python with ( current_directory(self.get_build_dir(arch.arch))), ( open(join('android', 'config.pxi'), 'w')) as fpxi, ( open(join('android', 'config.h'), 'w')) as fh, ( open(join('android', 'config.py'), 'w')) as fpy: for key, value in config.items(): fpxi.write(tpxi.format(key, repr(value))) fpy.write(tpy.format(key, repr(value))) fh.write(th.format( key, value if isinstance(value, int) else '"{}"'.format(value) )) self.config_env[key] = str(value) if is_sdl2: fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n') fh.write( '#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n' )
def handle_build_exception(exception): """ Handles a raised BuildInterruptingException by printing its error message and associated instructions, if any, then exiting. """ error('Build failed: {}'.format(exception.message)) if exception.instructions is not None: info('Instructions: {}'.format(exception.instructions)) exit(1)
def prebuild_arch(self, arch): super(AndroidRecipe, self).prebuild_arch(arch) tpxi = 'DEF {} = {}\n' th = '#define {} {}\n' tpy = '{} = {}\n' bootstrap = bootstrap_name = self.ctx.bootstrap.name is_sdl2 = bootstrap_name in ('sdl2', 'sdl2python3') is_pygame = bootstrap_name in ('pygame', ) is_webview = bootstrap_name in ('webview') is_lbry = bootstrap_name in ('lbry') if is_sdl2 or is_webview or is_lbry: if is_sdl2: bootstrap = 'sdl2' java_ns = 'org.kivy.android' jni_ns = 'org/kivy/android' elif is_pygame: java_ns = 'org.renpy.android' jni_ns = 'org/renpy/android' else: logger.error('unsupported bootstrap for android recipe: {}'.format( bootstrap_name)) exit(1) config = { 'BOOTSTRAP': bootstrap, 'IS_SDL2': int(is_sdl2), 'IS_PYGAME': int(is_pygame), 'PY2': int(will_build('python2')(self)), 'JAVA_NAMESPACE': java_ns, 'JNI_NAMESPACE': jni_ns, } with current_directory(self.get_build_dir(arch.arch)): with open(join('android', 'config.pxi'), 'w') as fpxi: with open(join('android', 'config.h'), 'w') as fh: with open(join('android', 'config.py'), 'w') as fpy: for key, value in config.items(): fpxi.write(tpxi.format(key, repr(value))) fpy.write(tpy.format(key, repr(value))) fh.write( th.format( key, value if isinstance(value, int) else '"{}"'.format(value))) self.config_env[key] = str(value) if is_sdl2: fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n') fh.write( '#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n' ) elif is_pygame: fh.write('JNIEnv *SDL_ANDROID_GetJNIEnv(void);\n')
def build_dist_from_args(ctx, dist, args): '''Parses out any bootstrap related arguments, and uses them to build a dist.''' bs = Bootstrap.get_bootstrap(args.bootstrap, ctx) build_order, python_modules, bs \ = get_recipe_order_and_bootstrap(ctx, dist.recipes, bs) ctx.recipe_build_order = build_order ctx.python_modules = python_modules if python_modules and hasattr(sys, 'real_prefix'): error('virtualenv is needed to install pure-Python modules, but') error('virtualenv does not support nesting, and you are running') error('python-for-android in one. Please run p4a outside of a') error('virtualenv instead.') exit(1) info('The selected bootstrap is {}'.format(bs.name)) info_main('# Creating dist with {} bootstrap'.format(bs.name)) bs.distribution = dist info_notify('Dist will have name {} and recipes ({})'.format( dist.name, ', '.join(dist.recipes))) ctx.dist_name = bs.distribution.name ctx.prepare_bootstrap(bs) ctx.prepare_dist(ctx.dist_name) build_recipes(build_order, python_modules, ctx) ctx.bootstrap.run_distribute() info_main('# Your distribution was created successfully, exiting.') info('Dist can be found at (for now) {}' .format(join(ctx.dist_dir, ctx.dist_name)))
def build_dist_from_args(ctx, dist, args): '''Parses out any bootstrap related arguments, and uses them to build a dist.''' bs = Bootstrap.get_bootstrap(args.bootstrap, ctx) build_order, python_modules, bs \ = get_recipe_order_and_bootstrap(ctx, dist.recipes, bs) ctx.recipe_build_order = build_order ctx.python_modules = python_modules if python_modules and hasattr(sys, 'real_prefix'): error('virtualenv is needed to install pure-Python modules, but') error('virtualenv does not support nesting, and you are running') error('python-for-android in one. Please run p4a outside of a') error('virtualenv instead.') exit(1) info('The selected bootstrap is {}'.format(bs.name)) info_main('# Creating dist with {} bootstrap'.format(bs.name)) bs.distribution = dist info_notify('Dist will have name {} and recipes ({})'.format( dist.name, ', '.join(dist.recipes))) info('Dist will also contain modules ({}) installed from pip'.format( ', '.join(ctx.python_modules))) ctx.dist_name = bs.distribution.name ctx.prepare_bootstrap(bs) ctx.prepare_dist(ctx.dist_name) build_recipes(build_order, python_modules, ctx) ctx.bootstrap.run_distribute() info_main('# Your distribution was created successfully, exiting.') info('Dist can be found at (for now) {}' .format(join(ctx.dist_dir, ctx.dist_name)))
def prebuild_arch(self, arch): super(AndroidRecipe, self).prebuild_arch(arch) tpxi = 'DEF {} = {}\n' th = '#define {} {}\n' tpy = '{} = {}\n' bootstrap = bootstrap_name = self.ctx.bootstrap.name is_sdl2 = bootstrap_name in ('sdl2', 'sdl2python3') is_pygame = bootstrap_name in ('pygame',) is_webview = bootstrap_name in ('webview',) if is_sdl2 or is_webview: if is_sdl2: bootstrap = 'sdl2' java_ns = 'org.kivy.android' jni_ns = 'org/kivy/android' elif is_pygame: java_ns = 'org.renpy.android' jni_ns = 'org/renpy/android' else: logger.error('unsupported bootstrap for android recipe: {}'.format(bootstrap_name)) exit(1) config = { 'BOOTSTRAP': bootstrap, 'IS_SDL2': int(is_sdl2), 'IS_PYGAME': int(is_pygame), 'PY2': int(will_build('python2')(self)), 'JAVA_NAMESPACE': java_ns, 'JNI_NAMESPACE': jni_ns, } with current_directory(self.get_build_dir(arch.arch)): with open(join('android', 'config.pxi'), 'w') as fpxi: with open(join('android', 'config.h'), 'w') as fh: with open(join('android', 'config.py'), 'w') as fpy: for key, value in config.items(): fpxi.write(tpxi.format(key, repr(value))) fpy.write(tpy.format(key, repr(value))) fh.write(th.format(key, value if isinstance(value, int) else '"{}"'.format(value))) self.config_env[key] = str(value) if is_sdl2: fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n') fh.write('#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n') elif is_pygame: fh.write('JNIEnv *SDL_ANDROID_GetJNIEnv(void);\n')
def build_arch(self, arch): # We don't have to actually build anything as CrystaX comes # with the necessary modules. They are included by modifying # the Android.mk in the jni folder. # If the Python version to be used is not prebuilt with the CrystaX # NDK, we do have to download it. crystax_python_dir = join(self.ctx.ndk_dir, 'sources', 'python') if not exists(join(crystax_python_dir, self.version)): info(('The NDK does not have a prebuilt Python {}, trying ' 'to obtain one.').format(self.version)) if self.version not in prebuilt_download_locations: error(('No prebuilt version for Python {} could be found, ' 'the built cannot continue.')) exit(1) with temp_directory() as td: self.download_file(prebuilt_download_locations[self.version], join(td, 'downloaded_python')) shprint(sh.tar, 'xf', join(td, 'downloaded_python'), '--directory', crystax_python_dir) if not exists(join(crystax_python_dir, self.version)): error(('Something went wrong, the directory at {} should ' 'have been created but does not exist.').format( join(crystax_python_dir, self.version))) if not exists(join(crystax_python_dir, self.version, 'libs', arch.arch)): error(('The prebuilt Python for version {} does not contain ' 'binaries for your chosen architecture "{}".').format( self.version, arch.arch)) exit(1) # TODO: We should have an option to build a new Python. This # would also allow linking to openssl and sqlite from CrystaX. dirn = self.ctx.get_python_install_dir() ensure_dir(dirn) # Instead of using a locally built hostpython, we use the # user's Python for now. They must have the right version # available. Using e.g. pyenv makes this easy. self.ctx.hostpython = 'python{}'.format(self.version)
def build_arch(self, arch): # We don't have to actually build anything as CrystaX comes # with the necessary modules. They are included by modifying # the Android.mk in the jni folder. # If the Python version to be used is not prebuilt with the CrystaX # NDK, we do have to download it. crystax_python_dir = join(self.ctx.ndk_dir, 'sources', 'python') if not exists(join(crystax_python_dir, self.version)): info(('The NDK does not have a prebuilt Python {}, trying ' 'to obtain one.').format(self.version)) if self.version not in prebuilt_download_locations: error(('No prebuilt version for Python {} could be found, ' 'the built cannot continue.')) exit(1) with temp_directory() as td: self.download_file(prebuilt_download_locations[self.version], join(td, 'downloaded_python')) shprint(sh.tar, 'xf', join(td, 'downloaded_python'), '--directory', crystax_python_dir) if not exists(join(crystax_python_dir, self.version)): error(('Something went wrong, the directory at {} should ' 'have been created but does not exist.').format( join(crystax_python_dir, self.version))) if not exists(join( crystax_python_dir, self.version, 'libs', arch.arch)): error(('The prebuilt Python for version {} does not contain ' 'binaries for your chosen architecture "{}".').format( self.version, arch.arch)) exit(1) # TODO: We should have an option to build a new Python. This # would also allow linking to openssl and sqlite from CrystaX. dirn = self.ctx.get_python_install_dir() ensure_dir(dirn) # Instead of using a locally built hostpython, we use the # user's Python for now. They must have the right version # available. Using e.g. pyenv makes this easy. self.ctx.hostpython = 'python{}'.format(self.version)
def prebuild_arch(self, arch): super().prebuild_arch(arch) ctx_bootstrap = self.ctx.bootstrap.name # define macros for Cython, C, Python tpxi = 'DEF {} = {}\n' th = '#define {} {}\n' tpy = '{} = {}\n' # make sure bootstrap name is in unicode if isinstance(ctx_bootstrap, bytes): ctx_bootstrap = ctx_bootstrap.decode('utf-8') bootstrap = bootstrap_name = ctx_bootstrap is_sdl2 = (bootstrap_name == "sdl2") if bootstrap_name in [ "sdl2", "webview", "service_only", "service_library" ]: java_ns = u'org.kivy.android' jni_ns = u'org/kivy/android' else: logger.error(('unsupported bootstrap for android recipe: {}' ''.format(bootstrap_name))) exit(1) config = { 'BOOTSTRAP': bootstrap, 'IS_SDL2': int(is_sdl2), 'PY2': 0, 'JAVA_NAMESPACE': java_ns, 'JNI_NAMESPACE': jni_ns, 'ACTIVITY_CLASS_NAME': self.ctx.activity_class_name, 'ACTIVITY_CLASS_NAMESPACE': self.ctx.activity_class_name.replace('.', '/'), 'SERVICE_CLASS_NAME': self.ctx.service_class_name, } # create config files for Cython, C and Python with (current_directory(self.get_build_dir(arch.arch))), (open( join('android', 'config.pxi'), 'w')) as fpxi, (open( join('android', 'config.h'), 'w')) as fh, (open(join('android', 'config.py'), 'w')) as fpy: for key, value in config.items(): fpxi.write(tpxi.format(key, repr(value))) fpy.write(tpy.format(key, repr(value))) fh.write( th.format( key, value if isinstance(value, int) else '"{}"'.format(value))) self.config_env[key] = str(value) if is_sdl2: fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n') fh.write( '#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n') else: fh.write('JNIEnv *WebView_AndroidGetJNIEnv(void);\n') fh.write( '#define SDL_ANDROID_GetJNIEnv WebView_AndroidGetJNIEnv\n')
def prepare_build_environment(self, user_sdk_dir, user_ndk_dir, user_android_api, user_ndk_ver): '''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 if sdk_dir is None: # This is the old P4A-specific var sdk_dir = environ.get('ANDROIDSDK', None) if sdk_dir is None: # This seems used more conventionally sdk_dir = environ.get('ANDROID_HOME', None) if sdk_dir is None: # Checks in the buildozer SDK dir, useful # for debug tests of p4a possible_dirs = glob.glob( expanduser( join('~', '.buildozer', 'android', 'platform', 'android-sdk-*'))) possible_dirs = [ d for d in possible_dirs if not (d.endswith('.bz2') or d.endswith('.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: warning('Android SDK dir was not specified, exiting.') exit(1) 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 if android_api is not None: info('Getting Android API version from user argument') if android_api is None: android_api = environ.get('ANDROIDAPI', None) if android_api is not None: info('Found Android API target in $ANDROIDAPI') if android_api is None: info('Android API target was not set manually, using ' 'the default of {}'.format(DEFAULT_ANDROID_API)) android_api = DEFAULT_ANDROID_API android_api = int(android_api) self.android_api = android_api if self.android_api >= 21 and self.archs[0].arch == 'armeabi': error('Asked to build for armeabi architecture with API ' '{}, but API 21 or greater does not support armeabi'.format( self.android_api)) error('You probably want to build with --arch=armeabi-v7a instead') exit(1) 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: error('Could not find `android` or `sdkmanager` binaries in ' 'Android SDK. Exiting.') 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] info('Available Android APIs are ({})'.format(', '.join(map(str, apis)))) if android_api in apis: info(('Requested API target {} is available, ' 'continuing.').format(android_api)) else: warning(('Requested API target {} is not available, install ' 'it with the SDK android tool.').format(android_api)) warning('Exiting.') exit(1) # Find the Android NDK # Could also use ANDROID_NDK, but doesn't look like many tools use this ndk_dir = None if user_ndk_dir: ndk_dir = user_ndk_dir if ndk_dir is not None: info('Getting NDK dir from from user argument') if ndk_dir is None: # The old P4A-specific dir ndk_dir = environ.get('ANDROIDNDK', None) if ndk_dir is not None: info('Found NDK dir in $ANDROIDNDK') if ndk_dir is None: # Apparently the most common convention ndk_dir = environ.get('NDK_HOME', None) if ndk_dir is not None: info('Found NDK dir in $NDK_HOME') if ndk_dir is None: # Another convention (with maven?) ndk_dir = environ.get('ANDROID_NDK_HOME', None) if ndk_dir is not None: info('Found NDK dir in $ANDROID_NDK_HOME') if ndk_dir is None: # Checks in the buildozer NDK dir, useful # # for debug tests of p4a possible_dirs = glob.glob( expanduser( join('~', '.buildozer', 'android', 'platform', 'android-ndk-r*'))) if possible_dirs: info('Found possible NDK dirs in buildozer dir: {}'.format( ', '.join([d.split(os.sep)[-1] for d in possible_dirs]))) info('Will attempt to use NDK at {}'.format(possible_dirs[0])) warning('This NDK lookup is intended for debug only, if you ' 'use python-for-android much you should probably ' 'maintain your own NDK download.') ndk_dir = possible_dirs[0] if ndk_dir is None: warning('Android NDK dir was not specified, exiting.') exit(1) self.ndk_dir = realpath(ndk_dir) # Find the NDK version, and check it against what the NDK dir # seems to report ndk_ver = None if user_ndk_ver: ndk_ver = user_ndk_ver if ndk_dir is not None: info('Got NDK version from from user argument') if ndk_ver is None: ndk_ver = environ.get('ANDROIDNDKVER', None) if ndk_dir is not None: info('Got NDK version from $ANDROIDNDKVER') self.ndk = 'google' try: with open(join(ndk_dir, 'RELEASE.TXT')) as fileh: reported_ndk_ver = fileh.read().split(' ')[0].strip() except IOError: pass else: if reported_ndk_ver.startswith('crystax-ndk-'): reported_ndk_ver = reported_ndk_ver[12:] self.ndk = 'crystax' if ndk_ver is None: ndk_ver = reported_ndk_ver info(('Got Android NDK version from the NDK dir: ' 'it is {}').format(ndk_ver)) else: if ndk_ver != reported_ndk_ver: warning('NDK version was set as {}, but checking ' 'the NDK dir claims it is {}.'.format( ndk_ver, reported_ndk_ver)) warning('The build will try to continue, but it may ' 'fail and you should check ' 'that your setting is correct.') warning('If the NDK dir result is correct, you don\'t ' 'need to manually set the NDK ver.') if ndk_ver is None: warning('Android NDK version could not be found. This probably' 'won\'t cause any problems, but if necessary you can' 'set it with `--ndk-version=...`.') self.ndk_ver = ndk_ver info('Using {} NDK {}'.format(self.ndk.capitalize(), self.ndk_ver)) virtualenv = None if virtualenv is None: virtualenv = sh.which('virtualenv2') if virtualenv is None: virtualenv = sh.which('virtualenv-2.7') if virtualenv is None: virtualenv = sh.which('virtualenv') if virtualenv is None: raise IOError('Couldn\'t find a virtualenv executable, ' 'you must install this to use p4a.') self.virtualenv = virtualenv info('Found virtualenv at {}'.format(virtualenv)) # path to some tools self.ccache = sh.which("ccache") if not self.ccache: info('ccache is missing, the build will not be optimized in the ' 'future.') for cython_fn in ("cython2", "cython-2.7", "cython"): cython = sh.which(cython_fn) if cython: self.cython = cython break else: error('No cython binary found. Exiting.') exit(1) if not self.cython: ok = False warning("Missing requirement: cython is not installed") # This would need to be changed if supporting multiarch APKs arch = self.archs[0] platform_dir = arch.platform_dir toolchain_prefix = arch.toolchain_prefix toolchain_version = None self.ndk_platform = join(self.ndk_dir, 'platforms', 'android-{}'.format(self.android_api), platform_dir) if not exists(self.ndk_platform): warning('ndk_platform doesn\'t exist: {}'.format( self.ndk_platform)) ok = False py_platform = sys.platform if py_platform in ['linux2', 'linux3']: py_platform = 'linux' toolchain_versions = [] toolchain_path = join(self.ndk_dir, 'toolchains') if os.path.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!') ok = False toolchain_versions.sort() toolchain_versions_gcc = [] for toolchain_version in toolchain_versions: if toolchain_version[0].isdigit(): # GCC toolchains begin with a number toolchain_versions_gcc.append(toolchain_version) if toolchain_versions: info('Found the following toolchain versions: {}'.format( toolchain_versions)) info('Picking the latest gcc toolchain, here {}'.format( toolchain_versions_gcc[-1])) toolchain_version = toolchain_versions_gcc[-1] else: warning('Could not find any toolchain for {}!'.format( toolchain_prefix)) ok = False self.toolchain_prefix = toolchain_prefix self.toolchain_version = toolchain_version # Modify the path so that sh finds modules appropriately environ['PATH'] = ( '{ndk_dir}/toolchains/{toolchain_prefix}-{toolchain_version}/' 'prebuilt/{py_platform}-x86/bin/:{ndk_dir}/toolchains/' '{toolchain_prefix}-{toolchain_version}/prebuilt/' '{py_platform}-x86_64/bin/:{ndk_dir}:{sdk_dir}/' 'tools:{path}').format(sdk_dir=self.sdk_dir, ndk_dir=self.ndk_dir, toolchain_prefix=toolchain_prefix, toolchain_version=toolchain_version, py_platform=py_platform, path=environ.get('PATH')) for executable in ("pkg-config", "autoconf", "automake", "libtoolize", "tar", "bzip2", "unzip", "make", "gcc", "g++"): if not sh.which(executable): warning("Missing executable: {} is not installed".format( executable)) if not ok: error('{}python-for-android cannot continue; aborting{}'.format( Err_Fore.RED, Err_Fore.RESET)) sys.exit(1)
def apk(self, args): '''Create an APK using the given distribution.''' ctx = self.ctx dist = self._dist # Manually fixing these arguments at the string stage is # unsatisfactory and should probably be changed somehow, but # we can't leave it until later as the build.py scripts assume # they are in the current directory. fix_args = ('--dir', '--private', '--add-jar', '--add-source', '--whitelist', '--blacklist', '--presplash', '--icon') unknown_args = args.unknown_args for i, arg in enumerate(unknown_args[:-1]): argx = arg.split('=') if argx[0] in fix_args: if len(argx) > 1: unknown_args[i] = '='.join((argx[0], realpath(expanduser(argx[1])))) else: unknown_args[i+1] = realpath(expanduser(unknown_args[i+1])) env = os.environ.copy() if args.build_mode == 'release': if args.keystore: env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore)) if args.signkey: env['P4A_RELEASE_KEYALIAS'] = args.signkey if args.keystorepw: env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw if args.signkeypw: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw build = imp.load_source('build', join(dist.dist_dir, 'build.py')) with current_directory(dist.dist_dir): self.hook("before_apk_build") build_args = build.parse_args(args.unknown_args) self.hook("after_apk_build") self.hook("before_apk_assemble") try: ant = sh.Command('ant') except sh.CommandNotFound: error('Could not find ant binary, please install it and make ' 'sure it is in your $PATH.') exit(1) output = shprint(ant, args.build_mode, _tail=20, _critical=True, _env=env) self.hook("after_apk_assemble") info_main('# Copying APK to current directory') apk_re = re.compile(r'.*Package: (.*\.apk)$') apk_file = None for line in reversed(output.splitlines()): m = apk_re.match(line) if m: apk_file = m.groups()[0] break if not apk_file: info_main('# APK filename not found in build output, trying to guess') suffix = args.build_mode if suffix == 'release' and not args.keystore: suffix = suffix + '-unsigned' apks = glob.glob(join(dist.dist_dir, 'bin', '*-*-{}.apk'.format(suffix))) if len(apks) == 0: raise ValueError('Couldn\'t find the built APK') if len(apks) > 1: info('More than one built APK found...guessing you ' 'just built {}'.format(apks[-1])) apk_file = apks[-1] info_main('# Found APK file: {}'.format(apk_file)) shprint(sh.cp, apk_file, './')
def get_distribution(cls, ctx, name=None, recipes=[], ndk_api=None, force_build=False, extra_dist_dirs=[], require_perfect_match=False, allow_replace_dist=True): '''Takes information about the distribution, and decides what kind of distribution it will be. If parameters conflict (e.g. a dist with that name already exists, but doesn't have the right set of recipes), an error is thrown. Parameters ---------- name : str The name of the distribution. If a dist with this name already ' exists, it will be used. recipes : list The recipes that the distribution must contain. force_download: bool If True, only downloaded dists are considered. force_build : bool If True, the dist is forced to be built locally. extra_dist_dirs : list Any extra directories in which to search for dists. require_perfect_match : bool If True, will only match distributions with precisely the correct set of recipes. allow_replace_dist : bool If True, will allow an existing dist with the specified name but incompatible requirements to be overwritten by a new one with the current requirements. ''' existing_dists = Distribution.get_distributions(ctx) needs_build = True # whether the dist needs building, will be returned possible_dists = existing_dists name_match_dist = None # 0) Check if a dist with that name already exists if name is not None and name: possible_dists = [d for d in possible_dists if d.name == name] if possible_dists: name_match_dist = possible_dists[0] # 1) Check if any existing dists meet the requirements _possible_dists = [] for dist in possible_dists: if ndk_api is not None and dist.ndk_api != ndk_api: continue for recipe in recipes: if recipe not in dist.recipes: break else: _possible_dists.append(dist) possible_dists = _possible_dists if possible_dists: info('Of the existing distributions, the following meet ' 'the given requirements:') pretty_log_dists(possible_dists) else: info('No existing dists meet the given requirements!') # If any dist has perfect recipes and ndk API, return it for dist in possible_dists: if force_build: continue if ndk_api is not None and dist.ndk_api != ndk_api: continue if (set(dist.recipes) == set(recipes) or (set(recipes).issubset(set(dist.recipes)) and not require_perfect_match)): info_notify('{} has compatible recipes, using this one'.format( dist.name)) return dist assert len(possible_dists) < 2 # If there was a name match but we didn't already choose it, # then the existing dist is incompatible with the requested # configuration and the build cannot continue if name_match_dist is not None and not allow_replace_dist: error( 'Asked for dist with name {name} with recipes ({req_recipes}) and ' 'NDK API {req_ndk_api}, but a dist ' 'with this name already exists and has either incompatible recipes ' '({dist_recipes}) or NDK API {dist_ndk_api}'.format( name=name, req_ndk_api=ndk_api, dist_ndk_api=name_match_dist.ndk_api, req_recipes=', '.join(recipes), dist_recipes=', '.join(name_match_dist.recipes))) error('No compatible dist found, so exiting.') exit(1) # If we got this far, we need to build a new dist dist = Distribution(ctx) dist.needs_build = True if not name: filen = 'unnamed_dist_{}' i = 1 while exists(join(ctx.dist_dir, filen.format(i))): i += 1 name = filen.format(i) dist.name = name dist.dist_dir = join(ctx.dist_dir, dist.name) dist.recipes = recipes dist.ndk_api = ctx.ndk_api return dist
def prepare_build_environment(self, user_sdk_dir, user_ndk_dir, user_android_api, user_ndk_ver): '''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 # AND: This needs revamping to carefully check each dependency # in turn ok = True # Work out where the Android SDK is sdk_dir = None if user_sdk_dir: sdk_dir = user_sdk_dir if sdk_dir is None: # This is the old P4A-specific var sdk_dir = environ.get('ANDROIDSDK', None) if sdk_dir is None: # This seems used more conventionally sdk_dir = environ.get('ANDROID_HOME', None) if sdk_dir is None: # Checks in the buildozer SDK dir, useful # # for debug tests of p4a possible_dirs = glob.glob(expanduser(join( '~', '.buildozer', 'android', 'platform', 'android-sdk-*'))) 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: warning('Android SDK dir was not specified, exiting.') exit(1) 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 if android_api is not None: info('Getting Android API version from user argument') if android_api is None: android_api = environ.get('ANDROIDAPI', None) if android_api is not None: info('Found Android API target in $ANDROIDAPI') if android_api is None: info('Android API target was not set manually, using ' 'the default of {}'.format(DEFAULT_ANDROID_API)) android_api = DEFAULT_ANDROID_API android_api = int(android_api) self.android_api = android_api if self.android_api >= 21 and self.archs[0].arch == 'armeabi': error('Asked to build for armeabi architecture with API ' '{}, but API 21 or greater does not support armeabi'.format( self.android_api)) error('You probably want to build with --arch=armeabi-v7a instead') exit(1) android = sh.Command(join(sdk_dir, 'tools', 'android')) targets = android('list').stdout.decode('utf-8').split('\n') 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] info('Available Android APIs are ({})'.format( ', '.join(map(str, apis)))) if android_api in apis: info(('Requested API target {} is available, ' 'continuing.').format(android_api)) else: warning(('Requested API target {} is not available, install ' 'it with the SDK android tool.').format(android_api)) warning('Exiting.') exit(1) # Find the Android NDK # Could also use ANDROID_NDK, but doesn't look like many tools use this ndk_dir = None if user_ndk_dir: ndk_dir = user_ndk_dir if ndk_dir is not None: info('Getting NDK dir from from user argument') if ndk_dir is None: # The old P4A-specific dir ndk_dir = environ.get('ANDROIDNDK', None) if ndk_dir is not None: info('Found NDK dir in $ANDROIDNDK') if ndk_dir is None: # Apparently the most common convention ndk_dir = environ.get('NDK_HOME', None) if ndk_dir is not None: info('Found NDK dir in $NDK_HOME') if ndk_dir is None: # Another convention (with maven?) ndk_dir = environ.get('ANDROID_NDK_HOME', None) if ndk_dir is not None: info('Found NDK dir in $ANDROID_NDK_HOME') if ndk_dir is None: # Checks in the buildozer NDK dir, useful # # for debug tests of p4a possible_dirs = glob.glob(expanduser(join( '~', '.buildozer', 'android', 'platform', 'android-ndk-r*'))) if possible_dirs: info('Found possible NDK dirs in buildozer dir: {}'.format( ', '.join([d.split(os.sep)[-1] for d in possible_dirs]))) info('Will attempt to use NDK at {}'.format(possible_dirs[0])) warning('This NDK lookup is intended for debug only, if you ' 'use python-for-android much you should probably ' 'maintain your own NDK download.') ndk_dir = possible_dirs[0] if ndk_dir is None: warning('Android NDK dir was not specified, exiting.') exit(1) self.ndk_dir = realpath(ndk_dir) # Find the NDK version, and check it against what the NDK dir # seems to report ndk_ver = None if user_ndk_ver: ndk_ver = user_ndk_ver if ndk_dir is not None: info('Got NDK version from from user argument') if ndk_ver is None: ndk_ver = environ.get('ANDROIDNDKVER', None) if ndk_dir is not None: info('Got NDK version from $ANDROIDNDKVER') self.ndk = 'google' try: with open(join(ndk_dir, 'RELEASE.TXT')) as fileh: reported_ndk_ver = fileh.read().split(' ')[0].strip() except IOError: pass else: if reported_ndk_ver.startswith('crystax-ndk-'): reported_ndk_ver = reported_ndk_ver[12:] self.ndk = 'crystax' if ndk_ver is None: ndk_ver = reported_ndk_ver info(('Got Android NDK version from the NDK dir: ' 'it is {}').format(ndk_ver)) else: if ndk_ver != reported_ndk_ver: warning('NDK version was set as {}, but checking ' 'the NDK dir claims it is {}.'.format( ndk_ver, reported_ndk_ver)) warning('The build will try to continue, but it may ' 'fail and you should check ' 'that your setting is correct.') warning('If the NDK dir result is correct, you don\'t ' 'need to manually set the NDK ver.') if ndk_ver is None: warning('Android NDK version could not be found, exiting.') self.ndk_ver = ndk_ver info('Using {} NDK {}'.format(self.ndk.capitalize(), self.ndk_ver)) virtualenv = None if virtualenv is None: virtualenv = sh.which('virtualenv2') if virtualenv is None: virtualenv = sh.which('virtualenv-2.7') if virtualenv is None: virtualenv = sh.which('virtualenv') if virtualenv is None: raise IOError('Couldn\'t find a virtualenv executable, ' 'you must install this to use p4a.') self.virtualenv = virtualenv info('Found virtualenv at {}'.format(virtualenv)) # path to some tools self.ccache = sh.which("ccache") if not self.ccache: info('ccache is missing, the build will not be optimized in the ' 'future.') for cython_fn in ("cython2", "cython-2.7", "cython"): cython = sh.which(cython_fn) if cython: self.cython = cython break else: error('No cython binary found. Exiting.') exit(1) if not self.cython: ok = False warning("Missing requirement: cython is not installed") # AND: need to change if supporting multiple archs at once arch = self.archs[0] platform_dir = arch.platform_dir toolchain_prefix = arch.toolchain_prefix self.ndk_platform = join( self.ndk_dir, 'platforms', 'android-{}'.format(self.android_api), platform_dir) if not exists(self.ndk_platform): warning('ndk_platform doesn\'t exist: {}'.format( self.ndk_platform)) ok = False py_platform = sys.platform if py_platform in ['linux2', 'linux3']: py_platform = 'linux' toolchain_versions = [] toolchain_path = join(self.ndk_dir, 'toolchains') if os.path.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!') ok = False toolchain_versions.sort() toolchain_versions_gcc = [] for toolchain_version in toolchain_versions: if toolchain_version[0].isdigit(): # GCC toolchains begin with a number toolchain_versions_gcc.append(toolchain_version) if toolchain_versions: info('Found the following toolchain versions: {}'.format( toolchain_versions)) info('Picking the latest gcc toolchain, here {}'.format( toolchain_versions_gcc[-1])) toolchain_version = toolchain_versions_gcc[-1] else: warning('Could not find any toolchain for {}!'.format( toolchain_prefix)) ok = False self.toolchain_prefix = toolchain_prefix self.toolchain_version = toolchain_version # Modify the path so that sh finds modules appropriately environ['PATH'] = ( '{ndk_dir}/toolchains/{toolchain_prefix}-{toolchain_version}/' 'prebuilt/{py_platform}-x86/bin/:{ndk_dir}/toolchains/' '{toolchain_prefix}-{toolchain_version}/prebuilt/' '{py_platform}-x86_64/bin/:{ndk_dir}:{sdk_dir}/' 'tools:{path}').format( sdk_dir=self.sdk_dir, ndk_dir=self.ndk_dir, toolchain_prefix=toolchain_prefix, toolchain_version=toolchain_version, py_platform=py_platform, path=environ.get('PATH')) for executable in ("pkg-config", "autoconf", "automake", "libtoolize", "tar", "bzip2", "unzip", "make", "gcc", "g++"): if not sh.which(executable): warning("Missing executable: {} is not installed".format( executable)) if not ok: error('{}python-for-android cannot continue; aborting{}'.format( Err_Fore.RED, Err_Fore.RESET)) sys.exit(1)
def get_recipe_order_and_bootstrap(ctx, names, bs=None): recipes_to_load = set(names) if bs is not None and bs.recipe_depends: recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) possible_orders = [] # get all possible order graphs, as names may include tuples/lists # of alternative dependencies names = [([name] if not isinstance(name, (list, tuple)) else name) for name in names] for name_set in product(*names): new_possible_orders = [RecipeOrder(ctx)] for name in name_set: new_possible_orders = recursively_collect_orders( name, ctx, orders=new_possible_orders) possible_orders.extend(new_possible_orders) # turn each order graph into a linear list if possible orders = [] for possible_order in possible_orders: try: order = find_order(possible_order) except ValueError: # a circular dependency was found info('Circular dependency found in graph {}, skipping it.'.format( possible_order)) continue except: warning('Failed to import recipe named {}; the recipe exists ' 'but appears broken.'.format(name)) warning('Exception was:') raise orders.append(list(order)) # prefer python2 and SDL2 if available orders = sorted(orders, key=lambda order: -('python2' in order) - ('sdl2' in order)) if not orders: error('Didn\'t find any valid dependency graphs.') error('This means that some of your requirements pull in ' 'conflicting dependencies.') error('Exiting.') exit(1) # It would be better to check against possible orders other # than the first one, but in practice clashes will be rare, # and can be resolved by specifying more parameters chosen_order = orders[0] if len(orders) > 1: info('Found multiple valid dependency orders:') for order in orders: info(' {}'.format(order)) info('Using the first of these: {}'.format(chosen_order)) else: info('Found a single valid recipe set: {}'.format(chosen_order)) if bs is None: bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx) recipes, python_modules, bs = get_recipe_order_and_bootstrap( ctx, chosen_order, bs=bs) else: # check if each requirement has a recipe recipes = [] python_modules = [] for name in chosen_order: try: Recipe.get_recipe(name, ctx) except IOError: python_modules.append(name) else: recipes.append(name) return recipes, python_modules, bs
def apk(self, args): '''Create an APK using the given distribution.''' ctx = self.ctx dist = self._dist # Manually fixing these arguments at the string stage is # unsatisfactory and should probably be changed somehow, but # we can't leave it until later as the build.py scripts assume # they are in the current directory. fix_args = ('--dir', '--private', '--add-jar', '--add-source', '--whitelist', '--blacklist', '--presplash', '--icon') unknown_args = args.unknown_args for i, arg in enumerate(unknown_args[:-1]): argx = arg.split('=') if argx[0] in fix_args: if len(argx) > 1: unknown_args[i] = '='.join( (argx[0], realpath(expanduser(argx[1])))) else: unknown_args[i + 1] = realpath( expanduser(unknown_args[i + 1])) env = os.environ.copy() if args.build_mode == 'release': if args.keystore: env['P4A_RELEASE_KEYSTORE'] = realpath( expanduser(args.keystore)) if args.signkey: env['P4A_RELEASE_KEYALIAS'] = args.signkey if args.keystorepw: env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw if args.signkeypw: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw build = imp.load_source('build', join(dist.dist_dir, 'build.py')) with current_directory(dist.dist_dir): self.hook("before_apk_build") build_args = build.parse_args(args.unknown_args) self.hook("after_apk_build") self.hook("before_apk_assemble") try: ant = sh.Command('ant') except sh.CommandNotFound: error('Could not find ant binary, please install it and make ' 'sure it is in your $PATH.') exit(1) output = shprint(ant, args.build_mode, _tail=20, _critical=True, _env=env) self.hook("after_apk_assemble") info_main('# Copying APK to current directory') apk_re = re.compile(r'.*Package: (.*\.apk)$') apk_file = None for line in reversed(output.splitlines()): m = apk_re.match(line) if m: apk_file = m.groups()[0] break if not apk_file: info_main( '# APK filename not found in build output, trying to guess') suffix = args.build_mode if suffix == 'release': suffix = suffix + '-unsigned' apks = glob.glob( join(dist.dist_dir, 'bin', '*-*-{}.apk'.format(suffix))) if len(apks) == 0: raise ValueError('Couldn\'t find the built APK') if len(apks) > 1: info('More than one built APK found...guessing you ' 'just built {}'.format(apks[-1])) apk_file = apks[-1] info_main('# Found APK file: {}'.format(apk_file)) shprint(sh.cp, apk_file, './')
def build_arch(self, arch): if self.ctx.ndk_api < self.MIN_NDK_API: error('Target ndk-api is {}, but the python3 recipe supports only {}+'.format( self.ctx.ndk_api, self.MIN_NDK_API)) exit(1) 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' # Skipping "Ensure that nl_langinfo is broken" from the original bpo-30386 platform_name = 'android-{}'.format(self.ctx.ndk_api) with current_directory(build_dir): env = environ.copy() # TODO: Get this information from p4a's arch system android_host = arch.command_prefix android_build = sh.Command(join(recipe_build_dir, 'config.guess'))().stdout.strip().decode('utf-8') platform_dir = join(self.ctx.ndk_dir, 'platforms', platform_name, arch.platform_dir) toolchain = '{android_host}-4.9'.format(android_host=arch.toolchain_prefix) toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain, 'prebuilt', 'linux-x86_64') target_data = arch.command_prefix.split('-') if target_data[0] == 'arm': target_data[0] = 'armv7a' target = '-'.join([target_data[0], 'none', target_data[1], target_data[2]]) CC = '{clang} -target {target} -gcc-toolchain {toolchain}'.format( clang=join(self.ctx.ndk_dir, 'toolchains', 'llvm', 'prebuilt', 'linux-x86_64', 'bin', 'clang'), target=target, toolchain=toolchain) AR = join(toolchain, 'bin', android_host) + '-ar' LD = join(toolchain, 'bin', android_host) + '-ld' RANLIB = join(toolchain, 'bin', android_host) + '-ranlib' READELF = join(toolchain, 'bin', android_host) + '-readelf' STRIP = join(toolchain, 'bin', android_host) + '-strip --strip-debug --strip-unneeded' env['CC'] = CC env['AR'] = AR env['LD'] = LD env['RANLIB'] = RANLIB env['READELF'] = READELF env['STRIP'] = STRIP env['PATH'] = '{hostpython_dir}:{old_path}'.format( hostpython_dir=self.get_recipe('hostpython3', self.ctx).get_path_to_python(), old_path=env['PATH']) ndk_flags = ('-fPIC --sysroot={ndk_sysroot} -D__ANDROID_API__={android_api} ' '-isystem {ndk_android_host}').format( ndk_sysroot=join(self.ctx.ndk_dir, 'sysroot'), android_api=self.ctx.ndk_api, ndk_android_host=join( self.ctx.ndk_dir, 'sysroot', 'usr', 'include', android_host)) sysroot = join(self.ctx.ndk_dir, 'platforms', platform_name, arch.platform_dir) env['CFLAGS'] = env.get('CFLAGS', '') + ' ' + ndk_flags env['CPPFLAGS'] = env.get('CPPFLAGS', '') + ' ' + ndk_flags env['LDFLAGS'] = env.get('LDFLAGS', '') + ' --sysroot={} -L{}'.format(sysroot, join(sysroot, 'usr', 'lib')) # Manually add the libs directory, and copy some object # files to the current directory otherwise they aren't # picked up. This seems necessary because the --sysroot # setting in LDFLAGS is overridden by the other flags. # TODO: Work out why this doesn't happen in the original # bpo-30386 Makefile system. logger.warning('Doing some hacky stuff to link properly') lib_dir = join(sysroot, 'usr', 'lib') if arch.arch == 'x86_64': lib_dir = join(sysroot, 'usr', 'lib64') env['LDFLAGS'] += ' -L{}'.format(lib_dir) shprint(sh.cp, join(lib_dir, 'crtbegin_so.o'), './') shprint(sh.cp, join(lib_dir, 'crtend_so.o'), './') env['SYSROOT'] = sysroot env = self.set_libs_flags(env, arch) if not exists('config.status'): shprint(sh.Command(join(recipe_build_dir, 'configure')), *(' '.join(('--host={android_host}', '--build={android_build}', '--enable-shared', '--disable-ipv6', 'ac_cv_file__dev_ptmx=yes', 'ac_cv_file__dev_ptc=no', '--without-ensurepip', 'ac_cv_little_endian_double=yes', '--prefix={prefix}', '--exec-prefix={exec_prefix}')).format( android_host=android_host, android_build=android_build, prefix=sys_prefix, exec_prefix=sys_exec_prefix)).split(' '), _env=env) if not exists('python'): shprint(sh.make, 'all', _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 get_recipe_order_and_bootstrap(ctx, names, bs=None): recipes_to_load = set(names) if bs is not None and bs.recipe_depends: recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) possible_orders = [] # get all possible order graphs, as names may include tuples/lists # of alternative dependencies names = [([name] if not isinstance(name, (list, tuple)) else name) for name in names] for name_set in product(*names): new_possible_orders = [RecipeOrder(ctx)] for name in name_set: new_possible_orders = recursively_collect_orders( name, ctx, orders=new_possible_orders) possible_orders.extend(new_possible_orders) # turn each order graph into a linear list if possible orders = [] for possible_order in possible_orders: try: order = find_order(possible_order) except ValueError: # a circular dependency was found info('Circular dependency found in graph {}, skipping it.'.format( possible_order)) continue except: warning('Failed to import recipe named {}; the recipe exists ' 'but appears broken.'.format(name)) warning('Exception was:') raise orders.append(list(order)) # prefer python2 and SDL2 if available orders = sorted(orders, key=lambda order: -('python2' in order) - ('sdl2' in order)) if not orders: error('Didn\'t find any valid dependency graphs.') error('This means that some of your requirements pull in ' 'conflicting dependencies.') error('Exiting.') exit(1) # It would be better to check against possible orders other # than the first one, but in practice clashes will be rare, # and can be resolved by specifying more parameters chosen_order = orders[0] if len(orders) > 1: info('Found multiple valid dependency orders:') for order in orders: info(' {}'.format(order)) info('Using the first of these: {}'.format(chosen_order)) else: info('Found a single valid recipe set: {}'.format(chosen_order)) if bs is None: bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx) recipes, python_modules, bs = get_recipe_order_and_bootstrap( ctx, chosen_order, bs=bs) else: # check if each requirement has a recipe recipes = [] python_modules = [] for name in chosen_order: try: recipe = Recipe.get_recipe(name, ctx) python_modules += recipe.python_depends except IOError: python_modules.append(name) else: recipes.append(name) python_modules = list(set(python_modules)) return recipes, python_modules, bs
def build_arch(self, arch): if self.ctx.ndk_api < self.MIN_NDK_API: error( 'Target ndk-api is {}, but the python3 recipe supports only {}+' .format(self.ctx.ndk_api, self.MIN_NDK_API)) exit(1) 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' # Skipping "Ensure that nl_langinfo is broken" from the original bpo-30386 platform_name = 'android-{}'.format(self.ctx.ndk_api) with current_directory(build_dir): env = environ.copy() # TODO: Get this information from p4a's arch system android_host = 'arm-linux-androideabi' android_build = sh.Command( join(recipe_build_dir, 'config.guess'))().stdout.strip().decode('utf-8') platform_dir = join(self.ctx.ndk_dir, 'platforms', platform_name, 'arch-arm') toolchain = '{android_host}-4.9'.format(android_host=android_host) toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain, 'prebuilt', 'linux-x86_64') CC = '{clang} -target {target} -gcc-toolchain {toolchain}'.format( clang=join(self.ctx.ndk_dir, 'toolchains', 'llvm', 'prebuilt', 'linux-x86_64', 'bin', 'clang'), target='armv7-none-linux-androideabi', toolchain=toolchain) AR = join(toolchain, 'bin', android_host) + '-ar' LD = join(toolchain, 'bin', android_host) + '-ld' RANLIB = join(toolchain, 'bin', android_host) + '-ranlib' READELF = join(toolchain, 'bin', android_host) + '-readelf' STRIP = join( toolchain, 'bin', android_host) + '-strip --strip-debug --strip-unneeded' env['CC'] = CC env['AR'] = AR env['LD'] = LD env['RANLIB'] = RANLIB env['READELF'] = READELF env['STRIP'] = STRIP env['PATH'] = '{hostpython_dir}:{old_path}'.format( hostpython_dir=self.get_recipe('hostpython3', self.ctx).get_path_to_python(), old_path=env['PATH']) ndk_flags = ( '--sysroot={ndk_sysroot} -D__ANDROID_API__={android_api} ' '-isystem {ndk_android_host}').format( ndk_sysroot=join(self.ctx.ndk_dir, 'sysroot'), android_api=self.ctx.ndk_api, ndk_android_host=join(self.ctx.ndk_dir, 'sysroot', 'usr', 'include', android_host)) sysroot = join(self.ctx.ndk_dir, 'platforms', platform_name, 'arch-arm') env['CFLAGS'] = env.get('CFLAGS', '') + ' ' + ndk_flags env['CPPFLAGS'] = env.get('CPPFLAGS', '') + ' ' + ndk_flags env['LDFLAGS'] = env.get('LDFLAGS', '') + ' --sysroot={} -L{}'.format( sysroot, join(sysroot, 'usr', 'lib')) # Manually add the libs directory, and copy some object # files to the current directory otherwise they aren't # picked up. This seems necessary because the --sysroot # setting in LDFLAGS is overridden by the other flags. # TODO: Work out why this doesn't happen in the original # bpo-30386 Makefile system. logger.warning('Doing some hacky stuff to link properly') lib_dir = join(sysroot, 'usr', 'lib') env['LDFLAGS'] += ' -L{}'.format(lib_dir) shprint(sh.cp, join(lib_dir, 'crtbegin_so.o'), './') shprint(sh.cp, join(lib_dir, 'crtend_so.o'), './') env['SYSROOT'] = sysroot env = self.set_libs_flags(env, arch) if not exists('config.status'): shprint(sh.Command(join(recipe_build_dir, 'configure')), *(' '.join( ('--host={android_host}', '--build={android_build}', '--enable-shared', '--disable-ipv6', 'ac_cv_file__dev_ptmx=yes', 'ac_cv_file__dev_ptc=no', '--without-ensurepip', 'ac_cv_little_endian_double=yes', '--prefix={prefix}', '--exec-prefix={exec_prefix}')).format( android_host=android_host, android_build=android_build, prefix=sys_prefix, exec_prefix=sys_exec_prefix)).split(' '), _env=env) if not exists('python'): shprint(sh.make, 'all', _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 apk(self, args): '''Create an APK using the given distribution.''' ctx = self.ctx dist = self._dist # Manually fixing these arguments at the string stage is # unsatisfactory and should probably be changed somehow, but # we can't leave it until later as the build.py scripts assume # they are in the current directory. fix_args = ('--dir', '--private', '--add-jar', '--add-source', '--whitelist', '--blacklist', '--presplash', '--icon') unknown_args = args.unknown_args for i, arg in enumerate(unknown_args[:-1]): argx = arg.split('=') if argx[0] in fix_args: if len(argx) > 1: unknown_args[i] = '='.join((argx[0], realpath(expanduser(argx[1])))) else: unknown_args[i+1] = realpath(expanduser(unknown_args[i+1])) env = os.environ.copy() if args.build_mode == 'release': if args.keystore: env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore)) if args.signkey: env['P4A_RELEASE_KEYALIAS'] = args.signkey if args.keystorepw: env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw if args.signkeypw: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw build = imp.load_source('build', join(dist.dist_dir, 'build.py')) with current_directory(dist.dist_dir): self.hook("before_apk_build") os.environ["ANDROID_API"] = str(self.ctx.android_api) build_args = build.parse_args(args.unknown_args) self.hook("after_apk_build") self.hook("before_apk_assemble") build_type = ctx.java_build_tool if build_type == 'auto': info('Selecting java build tool:') build_tools_versions = os.listdir(join(ctx.sdk_dir, 'build-tools')) build_tools_versions = sorted(build_tools_versions, key=LooseVersion) build_tools_version = build_tools_versions[-1] info(('Detected highest available build tools ' 'version to be {}').format(build_tools_version)) if build_tools_version >= '25.0' and exists('gradlew'): build_type = 'gradle' info(' Building with gradle, as gradle executable is present') else: build_type = 'ant' if build_tools_version < '25.0': info((' Building with ant, as the highest ' 'build-tools-version is only {}').format(build_tools_version)) else: info(' Building with ant, as no gradle executable detected') if build_type == 'gradle': # gradle-based build env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir env["ANDROID_HOME"] = self.ctx.sdk_dir gradlew = sh.Command('./gradlew') if args.build_mode == "debug": gradle_task = "assembleDebug" elif args.build_mode == "release": gradle_task = "assembleRelease" else: error("Unknown build mode {} for apk()".format( args.build_mode)) exit(1) output = shprint(gradlew, gradle_task, _tail=20, _critical=True, _env=env) # gradle output apks somewhere else # and don't have version in file apk_dir = join(dist.dist_dir, "build", "outputs", "apk") apk_glob = "*-{}.apk" apk_add_version = True else: # ant-based build try: ant = sh.Command('ant') except sh.CommandNotFound: error('Could not find ant binary, please install it ' 'and make sure it is in your $PATH.') exit(1) output = shprint(ant, args.build_mode, _tail=20, _critical=True, _env=env) apk_dir = join(dist.dist_dir, "bin") apk_glob = "*-*-{}.apk" apk_add_version = False self.hook("after_apk_assemble") info_main('# Copying APK to current directory') apk_re = re.compile(r'.*Package: (.*\.apk)$') apk_file = None for line in reversed(output.splitlines()): m = apk_re.match(line) if m: apk_file = m.groups()[0] break if not apk_file: info_main('# APK filename not found in build output, trying to guess') if args.build_mode == "release": suffixes = ("release", "release-unsigned") else: suffixes = ("debug", ) for suffix in suffixes: apks = glob.glob(join(apk_dir, apk_glob.format(suffix))) if apks: if len(apks) > 1: info('More than one built APK found... guessing you ' 'just built {}'.format(apks[-1])) apk_file = apks[-1] break else: raise ValueError('Couldn\'t find the built APK') info_main('# Found APK file: {}'.format(apk_file)) if apk_add_version: info('# Add version number to APK') apk_name, apk_suffix = basename(apk_file).split("-", 1) apk_file_dest = "{}-{}-{}".format( apk_name, build_args.version, apk_suffix) info('# APK renamed to {}'.format(apk_file_dest)) shprint(sh.cp, apk_file, apk_file_dest) else: shprint(sh.cp, apk_file, './')
def apk(self, args): '''Create an APK using the given distribution.''' ctx = self.ctx dist = self._dist # Manually fixing these arguments at the string stage is # unsatisfactory and should probably be changed somehow, but # we can't leave it until later as the build.py scripts assume # they are in the current directory. fix_args = ('--dir', '--private', '--add-jar', '--add-source', '--whitelist', '--blacklist', '--presplash', '--icon') unknown_args = args.unknown_args for i, arg in enumerate(unknown_args[:-1]): argx = arg.split('=') if argx[0] in fix_args: if len(argx) > 1: unknown_args[i] = '='.join((argx[0], realpath(expanduser(argx[1])))) else: unknown_args[i+1] = realpath(expanduser(unknown_args[i+1])) env = os.environ.copy() if args.build_mode == 'release': if args.keystore: env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore)) if args.signkey: env['P4A_RELEASE_KEYALIAS'] = args.signkey if args.keystorepw: env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw if args.signkeypw: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw build = imp.load_source('build', join(dist.dist_dir, 'build.py')) with current_directory(dist.dist_dir): self.hook("before_apk_build") os.environ["ANDROID_API"] = str(self.ctx.android_api) build_args = build.parse_args(args.unknown_args) self.hook("after_apk_build") self.hook("before_apk_assemble") build_type = ctx.java_build_tool if build_type == 'auto': info('Selecting java build tool:') build_tools_versions = os.listdir(join(ctx.sdk_dir, 'build-tools')) build_tools_versions = sorted(build_tools_versions, key=LooseVersion) build_tools_version = build_tools_versions[-1] info(('Detected highest available build tools ' 'version to be {}').format(build_tools_version)) if build_tools_version >= '25.0' and exists('gradlew'): build_type = 'gradle' info(' Building with gradle, as gradle executable is present') else: build_type = 'ant' if build_tools_version < '25.0': info((' Building with ant, as the highest ' 'build-tools-version is only {}').format(build_tools_version)) else: info(' Building with ant, as no gradle executable detected') elif build_type == 'gradle': # gradle-based build env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir env["ANDROID_HOME"] = self.ctx.sdk_dir gradlew = sh.Command('./gradlew') if args.build_mode == "debug": gradle_task = "assembleDebug" elif args.build_mode == "release": gradle_task = "assembleRelease" else: error("Unknown build mode {} for apk()".format( args.build_mode)) exit(1) output = shprint(gradlew, gradle_task, _tail=20, _critical=True, _env=env) # gradle output apks somewhere else # and don't have version in file apk_dir = join(dist.dist_dir, "build", "outputs", "apk") apk_glob = "*-{}.apk" apk_add_version = True elif build_type == 'none': return else: # ant-based build try: ant = sh.Command('ant') except sh.CommandNotFound: error('Could not find ant binary, please install it ' 'and make sure it is in your $PATH.') exit(1) output = shprint(ant, args.build_mode, _tail=20, _critical=True, _env=env) apk_dir = join(dist.dist_dir, "bin") apk_glob = "*-*-{}.apk" apk_add_version = False self.hook("after_apk_assemble") info_main('# Copying APK to current directory') apk_re = re.compile(r'.*Package: (.*\.apk)$') apk_file = None for line in reversed(output.splitlines()): m = apk_re.match(line) if m: apk_file = m.groups()[0] break if not apk_file: info_main('# APK filename not found in build output, trying to guess') if args.build_mode == "release": suffixes = ("release", "release-unsigned") else: suffixes = ("debug", ) for suffix in suffixes: apks = glob.glob(join(apk_dir, apk_glob.format(suffix))) if apks: if len(apks) > 1: info('More than one built APK found... guessing you ' 'just built {}'.format(apks[-1])) apk_file = apks[-1] break else: raise ValueError('Couldn\'t find the built APK') info_main('# Found APK file: {}'.format(apk_file)) if apk_add_version: info('# Add version number to APK') apk_name, apk_suffix = basename(apk_file).split("-", 1) apk_file_dest = "{}-{}-{}".format( apk_name, build_args.version, apk_suffix) info('# APK renamed to {}'.format(apk_file_dest)) shprint(sh.cp, apk_file, apk_file_dest) else: shprint(sh.cp, apk_file, './')
def get_distribution(cls, ctx, name=None, recipes=[], ndk_api=None, force_build=False, extra_dist_dirs=[], require_perfect_match=False, allow_replace_dist=True): '''Takes information about the distribution, and decides what kind of distribution it will be. If parameters conflict (e.g. a dist with that name already exists, but doesn't have the right set of recipes), an error is thrown. Parameters ---------- name : str The name of the distribution. If a dist with this name already ' exists, it will be used. recipes : list The recipes that the distribution must contain. force_download: bool If True, only downloaded dists are considered. force_build : bool If True, the dist is forced to be built locally. extra_dist_dirs : list Any extra directories in which to search for dists. require_perfect_match : bool If True, will only match distributions with precisely the correct set of recipes. allow_replace_dist : bool If True, will allow an existing dist with the specified name but incompatible requirements to be overwritten by a new one with the current requirements. ''' existing_dists = Distribution.get_distributions(ctx) needs_build = True # whether the dist needs building, will be returned possible_dists = existing_dists name_match_dist = None # 0) Check if a dist with that name already exists if name is not None and name: possible_dists = [d for d in possible_dists if d.name == name] if possible_dists: name_match_dist = possible_dists[0] # 1) Check if any existing dists meet the requirements _possible_dists = [] for dist in possible_dists: if ndk_api is not None and dist.ndk_api != ndk_api: continue for recipe in recipes: if recipe not in dist.recipes: break else: _possible_dists.append(dist) possible_dists = _possible_dists if possible_dists: info('Of the existing distributions, the following meet ' 'the given requirements:') pretty_log_dists(possible_dists) else: info('No existing dists meet the given requirements!') # If any dist has perfect recipes and ndk API, return it for dist in possible_dists: if force_build: continue if ndk_api is not None and dist.ndk_api != ndk_api: continue if (set(dist.recipes) == set(recipes) or (set(recipes).issubset(set(dist.recipes)) and not require_perfect_match)): info_notify('{} has compatible recipes, using this one' .format(dist.name)) return dist assert len(possible_dists) < 2 # If there was a name match but we didn't already choose it, # then the existing dist is incompatible with the requested # configuration and the build cannot continue if name_match_dist is not None and not allow_replace_dist: error('Asked for dist with name {name} with recipes ({req_recipes}) and ' 'NDK API {req_ndk_api}, but a dist ' 'with this name already exists and has either incompatible recipes ' '({dist_recipes}) or NDK API {dist_ndk_api}'.format( name=name, req_ndk_api=ndk_api, dist_ndk_api=name_match_dist.ndk_api, req_recipes=', '.join(recipes), dist_recipes=', '.join(name_match_dist.recipes))) error('No compatible dist found, so exiting.') exit(1) # If we got this far, we need to build a new dist dist = Distribution(ctx) dist.needs_build = True if not name: filen = 'unnamed_dist_{}' i = 1 while exists(join(ctx.dist_dir, filen.format(i))): i += 1 name = filen.format(i) dist.name = name dist.dist_dir = join(ctx.dist_dir, dist.name) dist.recipes = recipes dist.ndk_api = ctx.ndk_api return dist