2015-08-10 23:02:41 +08:00
#!/usr/bin/env python
import os , sys , subprocess , argparse , shutil , glob , re
import logging as log
import xml . etree . ElementTree as ET
2018-01-05 10:40:37 +08:00
SCRIPT_DIR = os . path . dirname ( os . path . abspath ( __file__ ) )
2015-08-10 23:02:41 +08:00
class Fail ( Exception ) :
def __init__ ( self , text = None ) :
self . t = text
def __str__ ( self ) :
return " ERROR " if self . t is None else self . t
2015-09-01 05:59:08 +08:00
def execute ( cmd , shell = False ) :
2015-08-10 23:02:41 +08:00
try :
2018-01-05 10:40:37 +08:00
log . debug ( " Executing: %s " % cmd )
log . info ( ' Executing: ' + ' ' . join ( cmd ) )
2015-09-01 05:59:08 +08:00
retcode = subprocess . call ( cmd , shell = shell )
2015-08-10 23:02:41 +08:00
if retcode < 0 :
raise Fail ( " Child was terminated by signal: " % s - retcode )
elif retcode > 0 :
raise Fail ( " Child returned: %s " % retcode )
except OSError as e :
2015-09-01 17:14:40 +08:00
raise Fail ( " Execution failed: %d / %s " % ( e . errno , e . strerror ) )
2015-08-10 23:02:41 +08:00
def rm_one ( d ) :
d = os . path . abspath ( d )
if os . path . exists ( d ) :
if os . path . isdir ( d ) :
log . info ( " Removing dir: %s " , d )
shutil . rmtree ( d )
elif os . path . isfile ( d ) :
log . info ( " Removing file: %s " , d )
os . remove ( d )
def check_dir ( d , create = False , clean = False ) :
d = os . path . abspath ( d )
log . info ( " Check dir %s (create: %s , clean: %s ) " , d , create , clean )
if os . path . exists ( d ) :
if not os . path . isdir ( d ) :
raise Fail ( " Not a directory: %s " % d )
if clean :
for x in glob . glob ( os . path . join ( d , " * " ) ) :
rm_one ( x )
else :
if create :
os . makedirs ( d )
return d
def determine_engine_version ( manifest_path ) :
with open ( manifest_path , " rt " ) as f :
return re . search ( r ' android:versionName= " ( \ d+ \ . \ d+) " ' , f . read ( ) , re . MULTILINE ) . group ( 1 )
def determine_opencv_version ( version_hpp_path ) :
# version in 2.4 - CV_VERSION_EPOCH.CV_VERSION_MAJOR.CV_VERSION_MINOR.CV_VERSION_REVISION
# version in master - CV_VERSION_MAJOR.CV_VERSION_MINOR.CV_VERSION_REVISION-CV_VERSION_STATUS
with open ( version_hpp_path , " rt " ) as f :
data = f . read ( )
major = re . search ( r ' ^#define \ W+CV_VERSION_MAJOR \ W+( \ d+)$ ' , data , re . MULTILINE ) . group ( 1 )
minor = re . search ( r ' ^#define \ W+CV_VERSION_MINOR \ W+( \ d+)$ ' , data , re . MULTILINE ) . group ( 1 )
revision = re . search ( r ' ^#define \ W+CV_VERSION_REVISION \ W+( \ d+)$ ' , data , re . MULTILINE ) . group ( 1 )
2015-12-18 22:38:04 +08:00
version_status = re . search ( r ' ^#define \ W+CV_VERSION_STATUS \ W+ " ([^ " ]*) " $ ' , data , re . MULTILINE ) . group ( 1 )
2015-08-10 23:02:41 +08:00
return " %(major)s . %(minor)s . %(revision)s %(version_status)s " % locals ( )
2018-01-04 03:07:55 +08:00
# shutil.move fails if dst exists
def move_smart ( src , dst ) :
def move_recurse ( subdir ) :
s = os . path . join ( src , subdir )
d = os . path . join ( dst , subdir )
if os . path . exists ( d ) :
if os . path . isdir ( d ) :
for item in os . listdir ( s ) :
move_recurse ( os . path . join ( subdir , item ) )
elif os . path . isfile ( s ) :
shutil . move ( s , d )
else :
shutil . move ( s , d )
move_recurse ( ' ' )
# shutil.copytree fails if dst exists
def copytree_smart ( src , dst ) :
def copy_recurse ( subdir ) :
s = os . path . join ( src , subdir )
d = os . path . join ( dst , subdir )
if os . path . exists ( d ) :
if os . path . isdir ( d ) :
for item in os . listdir ( s ) :
copy_recurse ( os . path . join ( subdir , item ) )
elif os . path . isfile ( s ) :
shutil . copy2 ( s , d )
else :
if os . path . isdir ( s ) :
shutil . copytree ( s , d )
elif os . path . isfile ( s ) :
shutil . copy2 ( s , d )
copy_recurse ( ' ' )
2015-08-10 23:02:41 +08:00
#===================================================================================================
class ABI :
2018-01-05 10:40:37 +08:00
def __init__ ( self , platform_id , name , toolchain , ndk_api_level = None , cmake_vars = dict ( ) ) :
2015-08-10 23:02:41 +08:00
self . platform_id = platform_id # platform code to add to apk version (for cmake)
self . name = name # general name (official Android ABI identifier)
self . toolchain = toolchain # toolchain identifier (for cmake)
2018-01-05 10:40:37 +08:00
self . cmake_vars = dict (
ANDROID_STL = " gnustl_static " ,
ANDROID_ABI = self . name ,
ANDROID_TOOLCHAIN_NAME = toolchain ,
ANDROID_PLATFORM_ID = platform_id ,
)
if ndk_api_level :
self . cmake_vars [ ' ANDROID_NATIVE_API_LEVEL ' ] = ndk_api_level
self . cmake_vars . update ( cmake_vars )
2015-08-10 23:02:41 +08:00
def __str__ ( self ) :
return " %s ( %s ) " % ( self . name , self . toolchain )
def haveIPP ( self ) :
2016-01-18 19:11:02 +08:00
return self . name == " x86 " or self . name == " x86_64 "
2015-08-10 23:02:41 +08:00
#===================================================================================================
class Builder :
2018-01-04 03:07:55 +08:00
def __init__ ( self , workdir , opencvdir , config ) :
2015-08-10 23:02:41 +08:00
self . workdir = check_dir ( workdir , create = True )
self . opencvdir = check_dir ( opencvdir )
2018-01-04 03:07:55 +08:00
self . config = config
2015-08-10 23:02:41 +08:00
self . libdest = check_dir ( os . path . join ( self . workdir , " o4a " ) , create = True , clean = True )
2018-01-04 03:07:55 +08:00
self . resultdest = check_dir ( os . path . join ( self . workdir , ' OpenCV-android-sdk ' ) , create = True , clean = True )
self . docdest = check_dir ( os . path . join ( self . workdir , ' OpenCV-android-sdk ' , ' sdk ' , ' java ' , ' javadoc ' ) , create = True , clean = True )
2015-08-10 23:02:41 +08:00
self . extra_packs = [ ]
self . opencv_version = determine_opencv_version ( os . path . join ( self . opencvdir , " modules " , " core " , " include " , " opencv2 " , " core " , " version.hpp " ) )
self . engine_version = determine_engine_version ( os . path . join ( self . opencvdir , " platforms " , " android " , " service " , " engine " , " AndroidManifest.xml " ) )
2018-01-05 10:40:37 +08:00
self . use_ccache = False if config . no_ccache else True
2015-08-10 23:02:41 +08:00
def get_toolchain_file ( self ) :
2018-01-05 10:40:37 +08:00
if not self . config . force_opencv_toolchain :
toolchain = os . path . join ( os . environ [ ' ANDROID_NDK ' ] , ' build ' , ' cmake ' , ' android.toolchain.cmake ' )
if os . path . exists ( toolchain ) :
return toolchain
toolchain = os . path . join ( SCRIPT_DIR , " android.toolchain.cmake " )
if os . path . exists ( toolchain ) :
return toolchain
else :
raise Fail ( " Can ' t find toolchain " )
2015-08-10 23:02:41 +08:00
def get_engine_apk_dest ( self , engdest ) :
return os . path . join ( engdest , " platforms " , " android " , " service " , " engine " , " .build " )
def add_extra_pack ( self , ver , path ) :
if path is None :
return
self . extra_packs . append ( ( ver , check_dir ( path ) ) )
def clean_library_build_dir ( self ) :
for d in [ " CMakeCache.txt " , " CMakeFiles/ " , " bin/ " , " libs/ " , " lib/ " , " package/ " , " install/samples/ " ] :
rm_one ( d )
def build_library ( self , abi , do_install ) :
2018-01-05 10:40:37 +08:00
cmd = [ " cmake " , " -GNinja " ]
cmake_vars = dict (
CMAKE_TOOLCHAIN_FILE = self . get_toolchain_file ( ) ,
WITH_OPENCL = " OFF " ,
WITH_CUDA = " OFF " ,
WITH_IPP = ( " ON " if abi . haveIPP ( ) else " OFF " ) ,
WITH_TBB = " ON " ,
BUILD_EXAMPLES = " OFF " ,
BUILD_TESTS = " OFF " ,
BUILD_PERF_TESTS = " OFF " ,
BUILD_DOCS = " OFF " ,
BUILD_ANDROID_EXAMPLES = " ON " ,
INSTALL_ANDROID_EXAMPLES = " ON " ,
)
if self . config . extra_modules_path is not None :
cmd . append ( " -DOPENCV_EXTRA_MODULES_PATH= ' %s ' " % self . config . extra_modules_path )
2016-04-23 06:09:07 +08:00
2015-08-10 23:02:41 +08:00
if self . use_ccache == True :
2016-01-18 19:11:02 +08:00
cmd . append ( " -DNDK_CCACHE=ccache " )
2015-08-10 23:02:41 +08:00
if do_install :
cmd . extend ( [ " -DBUILD_TESTS=ON " , " -DINSTALL_TESTS=ON " ] )
2018-01-05 10:40:37 +08:00
cmake_vars . update ( abi . cmake_vars )
cmd + = [ " -D %s = ' %s ' " % ( k , v ) for ( k , v ) in cmake_vars . items ( ) if v is not None ]
cmd . append ( self . opencvdir )
2015-08-10 23:02:41 +08:00
execute ( cmd )
if do_install :
execute ( [ " ninja " ] )
for c in [ " libs " , " dev " , " java " , " samples " ] :
execute ( [ " cmake " , " -DCOMPONENT= %s " % c , " -P " , " cmake_install.cmake " ] )
else :
execute ( [ " ninja " , " install/strip " ] )
def build_engine ( self , abi , engdest ) :
2018-01-05 10:40:37 +08:00
cmd = [ " cmake " , " -GNinja " ]
cmake_vars = dict (
CMAKE_TOOLCHAIN_FILE = self . get_toolchain_file ( ) ,
WITH_OPENCL = " OFF " ,
WITH_CUDA = " OFF " ,
WITH_IPP = " OFF " ,
BUILD_ANDROID_SERVICE = ' ON '
)
cmake_vars . update ( abi . cmake_vars )
cmd + = [ " -D %s = ' %s ' " % ( k , v ) for ( k , v ) in cmake_vars . items ( ) if v is not None ]
cmd . append ( self . opencvdir )
2015-08-10 23:02:41 +08:00
execute ( cmd )
apkdest = self . get_engine_apk_dest ( engdest )
2018-01-04 03:07:55 +08:00
assert os . path . exists ( apkdest ) , apkdest
2015-08-10 23:02:41 +08:00
# Add extra data
apkxmldest = check_dir ( os . path . join ( apkdest , " res " , " xml " ) , create = True )
apklibdest = check_dir ( os . path . join ( apkdest , " libs " , abi . name ) , create = True )
2018-02-19 22:30:06 +08:00
for ver , d in self . extra_packs + [ ( " 3.4.1 " , os . path . join ( self . libdest , " lib " ) ) ] :
2015-08-10 23:02:41 +08:00
r = ET . Element ( " library " , attrib = { " version " : ver } )
log . info ( " Adding libraries from %s " , d )
2015-09-01 17:14:40 +08:00
2015-08-10 23:02:41 +08:00
for f in glob . glob ( os . path . join ( d , abi . name , " *.so " ) ) :
log . info ( " Copy file: %s " , f )
shutil . copy2 ( f , apklibdest )
if " libnative_camera " in f :
continue
log . info ( " Register file: %s " , os . path . basename ( f ) )
n = ET . SubElement ( r , " file " , attrib = { " name " : os . path . basename ( f ) } )
2015-09-01 17:14:40 +08:00
if len ( list ( r ) ) > 0 :
xmlname = os . path . join ( apkxmldest , " config %s .xml " % ver . replace ( " . " , " " ) )
log . info ( " Generating XML config: %s " , xmlname )
ET . ElementTree ( r ) . write ( xmlname , encoding = " utf-8 " )
2015-08-10 23:02:41 +08:00
execute ( [ " ninja " , " opencv_engine " ] )
2015-09-01 17:14:40 +08:00
execute ( [ " ant " , " -f " , os . path . join ( apkdest , " build.xml " ) , " debug " ] ,
shell = ( sys . platform == ' win32 ' ) )
2015-08-10 23:02:41 +08:00
# TODO: Sign apk
def build_javadoc ( self ) :
2018-01-04 03:07:55 +08:00
classpaths = [ ]
2015-08-10 23:02:41 +08:00
for dir , _ , files in os . walk ( os . environ [ " ANDROID_SDK " ] ) :
for f in files :
if f == " android.jar " or f == " annotations.jar " :
classpaths . append ( os . path . join ( dir , f ) )
cmd = [
" javadoc " ,
" -header " , " OpenCV %s " % self . opencv_version ,
" -nodeprecated " ,
" -footer " , ' <a href= " http://docs.opencv.org " >OpenCV %s Documentation</a> ' % self . opencv_version ,
" -public " ,
2018-01-04 03:07:55 +08:00
' -sourcepath ' , os . path . join ( self . resultdest , ' sdk ' , ' java ' , ' src ' ) ,
2015-08-10 23:02:41 +08:00
" -d " , self . docdest ,
2018-01-04 03:07:55 +08:00
" -classpath " , " : " . join ( classpaths ) ,
' -subpackages ' , ' org.opencv ' ,
2015-08-10 23:02:41 +08:00
]
execute ( cmd )
def gather_results ( self , engines ) :
# Copy all files
root = os . path . join ( self . libdest , " install " )
for item in os . listdir ( root ) :
2018-01-04 03:07:55 +08:00
src = os . path . join ( root , item )
dst = os . path . join ( self . resultdest , item )
if os . path . isdir ( src ) :
2015-08-10 23:02:41 +08:00
log . info ( " Copy dir: %s " , item )
2018-01-04 03:07:55 +08:00
if self . config . force_copy :
copytree_smart ( src , dst )
else :
move_smart ( src , dst )
elif os . path . isfile ( src ) :
2015-08-10 23:02:41 +08:00
log . info ( " Copy file: %s " , item )
2018-01-04 03:07:55 +08:00
if self . config . force_copy :
shutil . copy2 ( src , dst )
else :
shutil . move ( src , dst )
2015-08-10 23:02:41 +08:00
# Copy engines for all platforms
for abi , engdest in engines :
log . info ( " Copy engine: %s ( %s ) " , abi , engdest )
f = os . path . join ( self . get_engine_apk_dest ( engdest ) , " bin " , " opencv_engine-debug.apk " )
resname = " OpenCV_ %s _Manager_ %s _ %s .apk " % ( self . opencv_version , self . engine_version , abi )
2018-01-04 03:07:55 +08:00
dst = os . path . join ( self . resultdest , " apk " , resname )
if self . config . force_copy :
shutil . copy2 ( f , dst )
else :
shutil . move ( f , dst )
2015-08-10 23:02:41 +08:00
# Clean samples
path = os . path . join ( self . resultdest , " samples " )
for item in os . listdir ( path ) :
item = os . path . join ( path , item )
if os . path . isdir ( item ) :
for name in [ " build.xml " , " local.properties " , " proguard-project.txt " ] :
rm_one ( os . path . join ( item , name ) )
#===================================================================================================
if __name__ == " __main__ " :
parser = argparse . ArgumentParser ( description = ' Build OpenCV for Android SDK ' )
2018-01-05 10:40:37 +08:00
parser . add_argument ( " work_dir " , nargs = ' ? ' , default = ' . ' , help = " Working directory (and output) " )
parser . add_argument ( " opencv_dir " , nargs = ' ? ' , default = os . path . join ( SCRIPT_DIR , ' ../.. ' ) , help = " Path to OpenCV source dir " )
parser . add_argument ( ' --config ' , default = ' ndk-10.config.py ' , type = str , help = " Package build configuration " , )
2015-08-10 23:02:41 +08:00
parser . add_argument ( ' --ndk_path ' , help = " Path to Android NDK to use for build " )
parser . add_argument ( ' --sdk_path ' , help = " Path to Android SDK to use for build " )
2016-04-23 06:09:07 +08:00
parser . add_argument ( " --extra_modules_path " , help = " Path to extra modules to use for build " )
2018-02-09 02:04:25 +08:00
parser . add_argument ( ' --sign_with ' , help = " Certificate to sign the Manager apk " )
2015-08-10 23:02:41 +08:00
parser . add_argument ( ' --build_doc ' , action = " store_true " , help = " Build javadoc " )
parser . add_argument ( ' --no_ccache ' , action = " store_true " , help = " Do not use ccache during library build " )
parser . add_argument ( ' --extra_pack ' , action = ' append ' , help = " provide extra OpenCV libraries for Manager apk in form <version>:<path-to-native-libs>, for example ' 2.4.11:unpacked/sdk/native/libs ' " )
2018-01-04 03:07:55 +08:00
parser . add_argument ( ' --force_copy ' , action = " store_true " , help = " Do not use file move during library build (useful for debug) " )
2018-01-05 10:40:37 +08:00
parser . add_argument ( ' --force_opencv_toolchain ' , action = " store_true " , help = " Do not use toolchain from Android NDK " )
2015-08-10 23:02:41 +08:00
args = parser . parse_args ( )
log . basicConfig ( format = ' %(message)s ' , level = log . DEBUG )
log . debug ( " Args: %s " , args )
if args . ndk_path is not None :
os . environ [ " ANDROID_NDK " ] = args . ndk_path
if args . sdk_path is not None :
os . environ [ " ANDROID_SDK " ] = args . sdk_path
2018-01-05 10:40:37 +08:00
if os . path . realpath ( args . work_dir ) == os . path . realpath ( SCRIPT_DIR ) :
raise Fail ( " Specify workdir (building from script directory is not supported) " )
if os . path . realpath ( args . work_dir ) == os . path . realpath ( args . opencv_dir ) :
raise Fail ( " Specify workdir (building from OpenCV source directory is not supported) " )
cpath = args . config
if not os . path . exists ( cpath ) :
cpath = os . path . join ( SCRIPT_DIR , cpath )
if not os . path . exists ( cpath ) :
raise Fail ( ' Config " %s " is missing ' % args . config )
with open ( cpath , ' r ' ) as f :
cfg = f . read ( )
print ( " Package configuration: " )
print ( ' = ' * 80 )
print ( cfg . strip ( ) )
print ( ' = ' * 80 )
exec ( compile ( cfg , cpath , ' exec ' ) )
2015-08-10 23:02:41 +08:00
log . info ( " Android NDK path: %s " , os . environ [ " ANDROID_NDK " ] )
log . info ( " Android SDK path: %s " , os . environ [ " ANDROID_SDK " ] )
2018-01-04 03:07:55 +08:00
builder = Builder ( args . work_dir , args . opencv_dir , args )
2015-08-10 23:02:41 +08:00
log . info ( " Detected OpenCV version: %s " , builder . opencv_version )
log . info ( " Detected Engine version: %s " , builder . engine_version )
2015-09-01 05:59:08 +08:00
if args . extra_pack :
for one in args . extra_pack :
i = one . find ( " : " )
if i > 0 and i < len ( one ) - 1 :
builder . add_extra_pack ( one [ : i ] , one [ i + 1 : ] )
else :
raise Fail ( " Bad extra pack provided: %s , should be in form ' <version>:<path-to-native-libs> ' " % one )
2015-08-10 23:02:41 +08:00
engines = [ ]
for i , abi in enumerate ( ABIs ) :
do_install = ( i == 0 )
engdest = check_dir ( os . path . join ( builder . workdir , " build_service_ %s " % abi . name ) , create = True , clean = True )
log . info ( " ===== " )
log . info ( " ===== Building library for %s " , abi )
log . info ( " ===== " )
os . chdir ( builder . libdest )
builder . clean_library_build_dir ( )
builder . build_library ( abi , do_install )
log . info ( " ===== " )
log . info ( " ===== Building engine for %s " , abi )
log . info ( " ===== " )
os . chdir ( engdest )
builder . build_engine ( abi , engdest )
engines . append ( ( abi . name , engdest ) )
2018-01-04 03:07:55 +08:00
builder . gather_results ( engines )
2015-08-10 23:02:41 +08:00
if args . build_doc :
builder . build_javadoc ( )
log . info ( " ===== " )
log . info ( " ===== Build finished " )
log . info ( " ===== " )
log . info ( " SDK location: %s " , builder . resultdest )
log . info ( " Documentation location: %s " , builder . docdest )