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