#!/usr/bin/env python3 # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import argparse import json import os import pathlib import shutil import subprocess import sys SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) REPO_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "..", "..")) BUILD_PY = os.path.join(REPO_DIR, "tools", "ci_build", "build.py") JAVA_ROOT = os.path.join(REPO_DIR, "java") sys.path.insert(0, os.path.join(REPO_DIR, "tools", "python")) from util import is_windows # noqa: E402 # We by default will build all 4 ABIs DEFAULT_BUILD_ABIS = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"] # Onnx Runtime native library is built against NDK API 21 by default # It is possible to build from source for Android API levels below 21, but it is not guaranteed DEFAULT_ANDROID_MIN_SDK_VER = 21 # Android API 24 is the default target API version for Android builds, based on Microsoft 1CS requirements # It is possible to build from source using API level 21 and higher as the target SDK version DEFAULT_ANDROID_TARGET_SDK_VER = 24 def _parse_build_settings(args): setting_file = args.build_settings_file.resolve() if not setting_file.is_file(): raise FileNotFoundError('Build config file {} is not a file.'.format(setting_file)) with open(setting_file) as f: build_settings_data = json.load(f) build_settings = {} if 'build_abis' in build_settings_data: build_settings['build_abis'] = build_settings_data['build_abis'] else: build_settings['build_abis'] = DEFAULT_BUILD_ABIS build_params = [] if 'build_params' in build_settings_data: build_params += build_settings_data['build_params'] else: raise ValueError('build_params is required in the build config file') if 'android_min_sdk_version' in build_settings_data: build_settings['android_min_sdk_version'] = build_settings_data['android_min_sdk_version'] else: build_settings['android_min_sdk_version'] = DEFAULT_ANDROID_MIN_SDK_VER build_params += ['--android_api=' + str(build_settings['android_min_sdk_version'])] if 'android_target_sdk_version' in build_settings_data: build_settings['android_target_sdk_version'] = build_settings_data['android_target_sdk_version'] else: build_settings['android_target_sdk_version'] = DEFAULT_ANDROID_TARGET_SDK_VER if build_settings['android_min_sdk_version'] > build_settings['android_target_sdk_version']: raise ValueError( 'android_min_sdk_version {} cannot be larger than android_target_sdk_version {}'.format( build_settings['android_min_sdk_version'], build_settings['android_target_sdk_version'] )) build_settings['build_params'] = build_params return build_settings def _build_aar(args): build_settings = _parse_build_settings(args) build_dir = os.path.abspath(args.build_dir) # Setup temp environment for building temp_env = os.environ.copy() temp_env['ANDROID_HOME'] = os.path.abspath(args.android_sdk_path) temp_env['ANDROID_NDK_HOME'] = os.path.abspath(args.android_ndk_path) # Temp dirs to hold building results intermediates_dir = os.path.join(build_dir, 'intermediates') build_config = args.config aar_dir = os.path.join(intermediates_dir, 'aar', build_config) jnilibs_dir = os.path.join(intermediates_dir, 'jnilibs', build_config) exe_dir = os.path.join(intermediates_dir, 'executables', build_config) base_build_command = [ sys.executable, BUILD_PY, '--config=' + build_config ] + build_settings['build_params'] header_files_path = '' # Build binary for each ABI, one by one for abi in build_settings['build_abis']: abi_build_dir = os.path.join(intermediates_dir, abi) abi_build_command = base_build_command + [ '--android_abi=' + abi, '--build_dir=' + abi_build_dir ] if args.include_ops_by_config is not None: abi_build_command += ['--include_ops_by_config=' + args.include_ops_by_config] subprocess.run(abi_build_command, env=temp_env, shell=False, check=True, cwd=REPO_DIR) # create symbolic links for libonnxruntime.so and libonnxruntime4j_jni.so # to jnilibs/[abi] for later compiling the aar package abi_jnilibs_dir = os.path.join(jnilibs_dir, abi) os.makedirs(abi_jnilibs_dir, exist_ok=True) for lib_name in ['libonnxruntime.so', 'libonnxruntime4j_jni.so']: target_lib_name = os.path.join(abi_jnilibs_dir, lib_name) # If the symbolic already exists, delete it first # For some reason, os.path.exists will return false for a symbolic link in Linux, # add double check with os.path.islink if os.path.exists(target_lib_name) or os.path.islink(target_lib_name): os.remove(target_lib_name) os.symlink(os.path.join(abi_build_dir, build_config, lib_name), target_lib_name) # copy executables for each abi, in case we want to publish those as well abi_exe_dir = os.path.join(exe_dir, abi) for exe_name in ['libonnxruntime.so', 'onnxruntime_perf_test', 'onnx_test_runner']: os.makedirs(abi_exe_dir, exist_ok=True) target_exe_name = os.path.join(abi_exe_dir, exe_name) shutil.copyfile(os.path.join(abi_build_dir, build_config, exe_name), target_exe_name) # we only need to define the header files path once if not header_files_path: header_files_path = os.path.join(abi_build_dir, build_config, 'android', 'headers') # The directory to publish final AAR aar_publish_dir = os.path.join(build_dir, 'aar_out', build_config) os.makedirs(aar_publish_dir, exist_ok=True) # get the common gradle command args gradle_command = [ 'gradle', '--no-daemon', '-b=build-android.gradle', '-c=settings-android.gradle', '-DjniLibsDir=' + jnilibs_dir, '-DbuildDir=' + aar_dir, '-DheadersDir=' + header_files_path, '-DpublishDir=' + aar_publish_dir, '-DminSdkVer=' + str(build_settings['android_min_sdk_version']), '-DtargetSdkVer=' + str(build_settings['android_target_sdk_version']) ] # If not using shell on Window, will not be able to find gradle in path use_shell = True if is_windows() else False # clean, build, and publish to a local directory subprocess.run(gradle_command + ['clean'], env=temp_env, shell=use_shell, check=True, cwd=JAVA_ROOT) subprocess.run(gradle_command + ['build'], env=temp_env, shell=use_shell, check=True, cwd=JAVA_ROOT) subprocess.run(gradle_command + ['publish'], env=temp_env, shell=use_shell, check=True, cwd=JAVA_ROOT) def parse_args(): parser = argparse.ArgumentParser( os.path.basename(__file__), description='''Create Android Archive (AAR) package for one or more Android ABI(s) and building properties specified in the given build config file, see tools/ci_build/github/android/default_mobile_aar_build_settings.json for details. The output of the final AAR package can be found under [build_dir]/aar_out ''' ) parser.add_argument("--android_sdk_path", type=str, default=os.environ.get("ANDROID_HOME", ""), help="Path to the Android SDK") parser.add_argument("--android_ndk_path", type=str, default=os.environ.get("ANDROID_NDK_HOME", ""), help="Path to the Android NDK") parser.add_argument('--build_dir', type=str, default=os.path.join(REPO_DIR, 'build/android_aar'), help='Provide the root directory for build output') parser.add_argument( "--include_ops_by_config", type=str, help="Include ops from config file. See /docs/Reduced_Operator_Kernel_build.md for more information.") parser.add_argument("--config", type=str, default="Release", choices=["Debug", "MinSizeRel", "Release", "RelWithDebInfo"], help="Configuration to build.") parser.add_argument('build_settings_file', type=pathlib.Path, help='Provide the file contains settings for building AAR') return parser.parse_args() def main(): args = parse_args() # Android SDK and NDK path are required if not args.android_sdk_path: raise ValueError('android_sdk_path is required') if not args.android_ndk_path: raise ValueError('android_ndk_path is required') _build_aar(args) if __name__ == '__main__': main()