1#!/bin/bash -eu 2# 3# Copyright (c) 2013 Google, Inc. 4# 5# This software is provided 'as-is', without any express or implied 6# warranty. In no event will the authors be held liable for any damages 7# arising from the use of this software. 8# Permission is granted to anyone to use this software for any purpose, 9# including commercial applications, and to alter it and redistribute it 10# freely, subject to the following restrictions: 11# 1. The origin of this software must not be misrepresented; you must not 12# claim that you wrote the original software. If you use this software 13# in a product, an acknowledgment in the product documentation would be 14# appreciated but is not required. 15# 2. Altered source versions must be plainly marked as such, and must not be 16# misrepresented as being the original software. 17# 3. This notice may not be removed or altered from any source distribution. 18# 19# Build, deploy, debug / execute a native Android package based upon 20# NativeActivity. 21 22declare -r script_directory=$(dirname $0) 23declare -r android_root=${script_directory}/../../../../../../ 24declare -r script_name=$(basename $0) 25declare -r android_manifest=AndroidManifest.xml 26declare -r os_name=$(uname -s) 27 28# Minimum Android target version supported by this project. 29: ${BUILDAPK_ANDROID_TARGET_MINVERSION:=10} 30# Directory containing the Android SDK 31# (http://developer.android.com/sdk/index.html). 32: ${ANDROID_SDK_HOME:=} 33# Directory containing the Android NDK 34# (http://developer.android.com/tools/sdk/ndk/index.html). 35: ${NDK_HOME:=} 36 37# Display script help and exit. 38usage() { 39 echo " 40Build the Android package in the current directory and deploy it to a 41connected device. 42 43Usage: ${script_name} \\ 44 [ADB_DEVICE=serial_number] [BUILD=0] [DEPLOY=0] [RUN_DEBUGGER=1] \ 45 [LAUNCH=0] [SWIG_BIN=swig_binary_directory] [SWIG_LIB=swig_include_directory] [ndk-build arguments ...] 46 47ADB_DEVICE=serial_number: 48 serial_number specifies the device to deploy the built apk to if multiple 49 Android devices are connected to the host. 50BUILD=0: 51 Disables the build of the package. 52DEPLOY=0: 53 Disables the deployment of the built apk to the Android device. 54RUN_DEBUGGER=1: 55 Launches the application in gdb after it has been deployed. To debug in 56 gdb, NDK_DEBUG=1 must also be specified on the command line to build a 57 debug apk. 58LAUNCH=0: 59 Disable the launch of the apk on the Android device. 60SWIG_BIN=swig_binary_directory: 61 The directory where the SWIG binary lives. No need to set this if SWIG is 62 installed and point to from your PATH variable. 63SWIG_LIB=swig_include_directory: 64 The directory where SWIG shared include files are, usually obtainable from 65 commandline with \"swig -swiglib\". No need to set this if SWIG is installed 66 and point to from your PATH variable. 67ndk-build arguments...: 68 Additional arguments for ndk-build. See ndk-build -h for more information. 69" >&2 70 exit 1 71} 72 73# Get the number of CPU cores present on the host. 74get_number_of_cores() { 75 case ${os_name} in 76 Darwin) 77 sysctl hw.ncpu | awk '{ print $2 }' 78 ;; 79 CYGWIN*|Linux) 80 awk '/^processor/ { n=$3 } END { print n + 1 }' /proc/cpuinfo 81 ;; 82 *) 83 echo 1 84 ;; 85 esac 86} 87 88# Get the package name from an AndroidManifest.xml file. 89get_package_name_from_manifest() { 90 xmllint --xpath 'string(/manifest/@package)' "${1}" 91} 92 93# Get the library name from an AndroidManifest.xml file. 94get_library_name_from_manifest() { 95 echo "\ 96setns android=http://schemas.android.com/apk/res/android 97xpath string(/manifest/application/activity\ 98[@android:name=\"android.app.NativeActivity\"]/meta-data\ 99[@android:name=\"android.app.lib_name\"]/@android:value)" | 100 xmllint --shell "${1}" | awk '/Object is a string/ { print $NF }' 101} 102 103# Get the number of Android devices connected to the system. 104get_number_of_devices_connected() { 105 adb devices -l | \ 106 awk '/^..*$/ { if (p) { print $0 } } 107 /List of devices attached/ { p = 1 }' | \ 108 wc -l 109 return ${PIPESTATUS[0]} 110} 111 112# Kill a process and its' children. This is provided for cygwin which 113# doesn't ship with pkill. 114kill_process_group() { 115 local parent_pid="${1}" 116 local child_pid= 117 for child_pid in $(ps -f | \ 118 awk '{ if ($3 == '"${parent_pid}"') { print $2 } }'); do 119 kill_process_group "${child_pid}" 120 done 121 kill "${parent_pid}" 2>/dev/null 122} 123 124# Find and run "adb". 125adb() { 126 local adb_path= 127 for path in "$(which adb 2>/dev/null)" \ 128 "${ANDROID_SDK_HOME}/sdk/platform-tools/adb" \ 129 "${android_root}/prebuilts/sdk/platform-tools/adb"; do 130 if [[ -e "${path}" ]]; then 131 adb_path="${path}" 132 break 133 fi 134 done 135 if [[ "${adb_path}" == "" ]]; then 136 echo -e "Unable to find adb." \ 137 "\nAdd the Android ADT sdk/platform-tools directory to the" \ 138 "PATH." >&2 139 exit 1 140 fi 141 "${adb_path}" "$@" 142} 143 144# Find and run "android". 145android() { 146 local android_executable=android 147 if echo "${os_name}" | grep -q CYGWIN; then 148 android_executable=android.bat 149 fi 150 local android_path= 151 for path in "$(which ${android_executable})" \ 152 "${ANDROID_SDK_HOME}/sdk/tools/${android_executable}" \ 153 "${android_root}/prebuilts/sdk/tools/${android_executable}"; do 154 if [[ -e "${path}" ]]; then 155 android_path="${path}" 156 break 157 fi 158 done 159 if [[ "${android_path}" == "" ]]; then 160 echo -e "Unable to find android tool." \ 161 "\nAdd the Android ADT sdk/tools directory to the PATH." >&2 162 exit 1 163 fi 164 # Make sure ant is installed. 165 if [[ "$(which ant)" == "" ]]; then 166 echo -e "Unable to find ant." \ 167 "\nPlease install ant and add to the PATH." >&2 168 exit 1 169 fi 170 171 "${android_path}" "$@" 172} 173 174# Find and run "ndk-build" 175ndkbuild() { 176 local ndkbuild_path= 177 for path in "$(which ndk-build 2>/dev/null)" \ 178 "${NDK_HOME}/ndk-build" \ 179 "${android_root}/prebuilts/ndk/current/ndk-build"; do 180 if [[ -e "${path}" ]]; then 181 ndkbuild_path="${path}" 182 break 183 fi 184 done 185 if [[ "${ndkbuild_path}" == "" ]]; then 186 echo -e "Unable to find ndk-build." \ 187 "\nAdd the Android NDK directory to the PATH." >&2 188 exit 1 189 fi 190 "${ndkbuild_path}" "$@" 191} 192 193# Get file modification time of $1 in seconds since the epoch. 194stat_mtime() { 195 local filename="${1}" 196 case ${os_name} in 197 Darwin) stat -f%m "${filename}" 2>/dev/null || echo 0 ;; 198 *) stat -c%Y "${filename}" 2>/dev/null || echo 0 ;; 199 esac 200} 201 202# Build the native (C/C++) build targets in the current directory. 203build_native_targets() { 204 # Save the list of output modules in the install directory so that it's 205 # possible to restore their timestamps after the build is complete. This 206 # works around a bug in ndk/build/core/setup-app.mk which results in the 207 # unconditional execution of the clean-installed-binaries rule. 208 restore_libraries="$(find libs -type f 2>/dev/null | \ 209 sed -E 's@^libs/(.*)@\1@')" 210 211 # Build native code. 212 ndkbuild -j$(get_number_of_cores) "$@" 213 214 # Restore installed libraries. 215 # Obviously this is a nasty hack (along with ${restore_libraries} above) as 216 # it assumes it knows where the NDK will be placing output files. 217 ( 218 IFS=$'\n' 219 for libpath in ${restore_libraries}; do 220 source_library="obj/local/${libpath}" 221 target_library="libs/${libpath}" 222 if [[ -e "${source_library}" ]]; then 223 cp -a "${source_library}" "${target_library}" 224 fi 225 done 226 ) 227} 228 229# Select the oldest installed android build target that is at least as new as 230# BUILDAPK_ANDROID_TARGET_MINVERSION. If a suitable build target isn't found, 231# this function prints an error message and exits with an error. 232select_android_build_target() { 233 local -r android_targets_installed=$( \ 234 android list targets | \ 235 awk -F'"' '/^id:.*android/ { print $2 }') 236 local android_build_target= 237 for android_target in $(echo "${android_targets_installed}" | \ 238 awk -F- '{ print $2 }' | sort -n); do 239 local isNumber='^[0-9]+$' 240 # skip preview API releases e.g. 'android-L' 241 if [[ $android_target =~ $isNumber ]]; then 242 if [[ $((android_target)) -ge \ 243 $((BUILDAPK_ANDROID_TARGET_MINVERSION)) ]]; then 244 android_build_target="android-${android_target}" 245 break 246 fi 247 # else 248 # The API version is a letter, so skip it. 249 fi 250 done 251 if [[ "${android_build_target}" == "" ]]; then 252 echo -e \ 253 "Found installed Android targets:" \ 254 "$(echo ${android_targets_installed} | sed 's/ /\n /g;s/^/\n /;')" \ 255 "\nAndroid SDK platform" \ 256 "android-$((BUILDAPK_ANDROID_TARGET_MINVERSION))" \ 257 "must be installed to build this project." \ 258 "\nUse the \"android\" application to install API" \ 259 "$((BUILDAPK_ANDROID_TARGET_MINVERSION)) or newer." >&2 260 exit 1 261 fi 262 echo "${android_build_target}" 263} 264 265# Sign unsigned apk $1 and write the result to $2 with key store file $3 and 266# password $4. 267# If a key store file $3 and password $4 aren't specified, a temporary 268# (60 day) key is generated and used to sign the package. 269sign_apk() { 270 local unsigned_apk="${1}" 271 local signed_apk="${2}" 272 if [[ $(stat_mtime "${unsigned_apk}") -gt \ 273 $(stat_mtime "${signed_apk}") ]]; then 274 local -r key_alias=$(basename ${signed_apk} .apk) 275 local keystore="${3}" 276 local key_password="${4}" 277 [[ "${keystore}" == "" ]] && keystore="${unsigned_apk}.keystore" 278 [[ "${key_password}" == "" ]] && \ 279 key_password="${key_alias}123456" 280 if [[ ! -e ${keystore} ]]; then 281 keytool -genkey -v -dname "cn=, ou=${key_alias}, o=fpl" \ 282 -storepass ${key_password} \ 283 -keypass ${key_password} -keystore ${keystore} \ 284 -alias ${key_alias} -keyalg RSA -keysize 2048 -validity 60 285 fi 286 cp "${unsigned_apk}" "${signed_apk}" 287 jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \ 288 -keystore ${keystore} -storepass ${key_password} \ 289 -keypass ${key_password} "${signed_apk}" ${key_alias} 290 fi 291} 292 293# Build the apk $1 for package filename $2 in the current directory using the 294# ant build target $3. 295build_apk() { 296 local -r output_apk="${1}" 297 local -r package_filename="${2}" 298 local -r ant_target="${3}" 299 # Get the list of installed android targets and select the oldest target 300 # that is at least as new as BUILDAPK_ANDROID_TARGET_MINVERSION. 301 local -r android_build_target=$(select_android_build_target) 302 [[ "${android_build_target}" == "" ]] && exit 1 303 echo "Building ${output_apk} for target ${android_build_target}" >&2 304 305 # Create / update build.xml and local.properties files. 306 if [[ $(stat_mtime "${android_manifest}") -gt \ 307 $(stat_mtime build.xml) ]]; then 308 android update project --target "${android_build_target}" \ 309 -n ${package_filename} --path . 310 fi 311 312 # Use ant to build the apk. 313 ant -quiet ${ant_target} 314 315 # Sign release apks with a temporary key as these packages will not be 316 # redistributed. 317 local unsigned_apk="bin/${package_filename}-${ant_target}-unsigned.apk" 318 if [[ "${ant_target}" == "release" ]]; then 319 sign_apk "${unsigned_apk}" "${output_apk}" "" "" 320 fi 321} 322 323# Uninstall package $1 and install apk $2 on device $3 where $3 is "-s device" 324# or an empty string. If $3 is an empty string adb will fail when multiple 325# devices are connected to the host system. 326install_apk() { 327 local -r uninstall_package_name="${1}" 328 local -r install_apk="${2}" 329 local -r adb_device="${3}" 330 # Uninstall the package if it's already installed. 331 adb ${adb_device} uninstall "${uninstall_package_name}" 1>&2 > /dev/null || \ 332 true # no error check 333 334 # Install the apk. 335 # NOTE: The following works around adb not returning an error code when 336 # it fails to install an apk. 337 echo "Install ${install_apk}" >&2 338 local -r adb_install_result=$(adb ${adb_device} install "${install_apk}") 339 echo "${adb_install_result}" 340 if echo "${adb_install_result}" | grep -qF 'Failure ['; then 341 exit 1 342 fi 343} 344 345# Launch previously installed package $1 on device $2. 346# If $2 is an empty string adb will fail when multiple devices are connected 347# to the host system. 348launch_package() { 349 ( 350 # Determine the SDK version of Android on the device. 351 local -r android_sdk_version=$( 352 adb ${adb_device} shell cat system/build.prop | \ 353 awk -F= '/ro.build.version.sdk/ { 354 v=$2; sub(/[ \r\n]/, "", v); print v 355 }') 356 357 # Clear logs from previous runs. 358 # Note that logcat does not just 'tail' the logs, it dumps the entire log 359 # history. 360 adb ${adb_device} logcat -c 361 362 local finished_msg='Displayed '"${package_name}" 363 local timeout_msg='Activity destroy timeout.*'"${package_name}" 364 # Maximum time to wait before stopping log monitoring. 0 = infinity. 365 local launch_timeout=0 366 # If this is a Gingerbread device, kill log monitoring after 10 seconds. 367 if [[ $((android_sdk_version)) -le 10 ]]; then 368 launch_timeout=10 369 fi 370 # Display logcat in the background. 371 # Stop displaying the log when the app launch / execution completes or the 372 # logcat 373 ( 374 adb ${adb_device} logcat | \ 375 awk " 376 { 377 print \$0 378 } 379 380 /ActivityManager.*: ${finished_msg}/ { 381 exit 0 382 } 383 384 /ActivityManager.*: ${timeout_msg}/ { 385 exit 0 386 }" & 387 adb_logcat_pid=$!; 388 if [[ $((launch_timeout)) -gt 0 ]]; then 389 sleep $((launch_timeout)); 390 kill ${adb_logcat_pid}; 391 else 392 wait ${adb_logcat_pid}; 393 fi 394 ) & 395 logcat_pid=$! 396 # Kill adb logcat if this shell exits. 397 trap "kill_process_group ${logcat_pid}" SIGINT SIGTERM EXIT 398 399 # If the SDK is newer than 10, "am" supports stopping an activity. 400 adb_stop_activity= 401 if [[ $((android_sdk_version)) -gt 10 ]]; then 402 adb_stop_activity=-S 403 fi 404 405 # Launch the activity and wait for it to complete. 406 adb ${adb_device} shell am start ${adb_stop_activity} -n \ 407 ${package_name}/android.app.NativeActivity 408 409 wait "${logcat_pid}" 410 ) 411} 412 413# See usage(). 414main() { 415 # Parse arguments for this script. 416 local adb_device= 417 local ant_target=release 418 local disable_deploy=0 419 local disable_build=0 420 local run_debugger=0 421 local launch=1 422 local build_package=1 423 for opt; do 424 case ${opt} in 425 # NDK_DEBUG=0 tells ndk-build to build this as debuggable but to not 426 # modify the underlying code whereas NDK_DEBUG=1 also builds as debuggable 427 # but does modify the code 428 NDK_DEBUG=1) ant_target=debug ;; 429 NDK_DEBUG=0) ant_target=debug ;; 430 ADB_DEVICE*) adb_device="$(\ 431 echo "${opt}" | sed -E 's/^ADB_DEVICE=([^ ]+)$/-s \1/;t;s/.*//')" ;; 432 BUILD=0) disable_build=1 ;; 433 DEPLOY=0) disable_deploy=1 ;; 434 RUN_DEBUGGER=1) run_debugger=1 ;; 435 LAUNCH=0) launch=0 ;; 436 clean) build_package=0 disable_deploy=1 launch=0 ;; 437 -h|--help|help) usage ;; 438 esac 439 done 440 441 # If a target device hasn't been specified and multiple devices are connected 442 # to the host machine, display an error. 443 local -r devices_connected=$(get_number_of_devices_connected) 444 if [[ "${adb_device}" == "" && $((devices_connected)) -gt 1 && \ 445 ($((disable_deploy)) -eq 0 || $((launch)) -ne 0 || \ 446 $((run_debugger)) -ne 0) ]]; then 447 if [[ $((disable_deploy)) -ne 0 ]]; then 448 echo "Deployment enabled, disable using DEPLOY=0" >&2 449 fi 450 if [[ $((launch)) -ne 0 ]]; then 451 echo "Launch enabled." >&2 452 fi 453 if [[ $((disable_deploy)) -eq 0 ]]; then 454 echo "Deployment enabled." >&2 455 fi 456 if [[ $((run_debugger)) -ne 0 ]]; then 457 echo "Debugger launch enabled." >&2 458 fi 459 echo " 460Multiple Android devices are connected to this host. Either disable deployment 461and execution of the built .apk using: 462 \"${script_name} DEPLOY=0 LAUNCH=0\" 463 464or specify a device to deploy to using: 465 \"${script_name} ADB_DEVICE=\${device_serial}\". 466 467The Android devices connected to this machine are: 468$(adb devices -l) 469" >&2 470 exit 1 471 fi 472 473 if [[ $((disable_build)) -eq 0 ]]; then 474 # Build the native target. 475 build_native_targets "$@" 476 fi 477 478 # Get the package name from the manifest. 479 local -r package_name=$(get_package_name_from_manifest "${android_manifest}") 480 if [[ "${package_name}" == "" ]]; then 481 echo -e "No package name specified in ${android_manifest},"\ 482 "skipping apk build, deploy" 483 "\nand launch steps." >&2 484 exit 0 485 fi 486 local -r package_basename=${package_name/*./} 487 local package_filename=$(get_library_name_from_manifest ${android_manifest}) 488 [[ "${package_filename}" == "" ]] && package_filename="${package_basename}" 489 490 # Output apk name. 491 local -r output_apk="bin/${package_filename}-${ant_target}.apk" 492 493 if [[ $((disable_build)) -eq 0 && $((build_package)) -eq 1 ]]; then 494 # Build the apk. 495 build_apk "${output_apk}" "${package_filename}" "${ant_target}" 496 fi 497 498 # Deploy to the device. 499 if [[ $((disable_deploy)) -eq 0 ]]; then 500 install_apk "${package_name}" "${output_apk}" "${adb_device}" 501 fi 502 503 if [[ "${ant_target}" == "debug" && $((run_debugger)) -eq 1 ]]; then 504 # Start debugging. 505 ndk-gdb ${adb_device} --start 506 elif [[ $((launch)) -eq 1 ]]; then 507 launch_package "${package_name}" "${adb_device}" 508 fi 509} 510 511main "$@" 512