You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
511 lines
17 KiB
511 lines
17 KiB
#!/bin/bash -eu |
|
# |
|
# Copyright (c) 2013 Google, Inc. |
|
# |
|
# This software is provided 'as-is', without any express or implied |
|
# warranty. In no event will the authors be held liable for any damages |
|
# arising from the use of this software. |
|
# Permission is granted to anyone to use this software for any purpose, |
|
# including commercial applications, and to alter it and redistribute it |
|
# freely, subject to the following restrictions: |
|
# 1. The origin of this software must not be misrepresented; you must not |
|
# claim that you wrote the original software. If you use this software |
|
# in a product, an acknowledgment in the product documentation would be |
|
# appreciated but is not required. |
|
# 2. Altered source versions must be plainly marked as such, and must not be |
|
# misrepresented as being the original software. |
|
# 3. This notice may not be removed or altered from any source distribution. |
|
# |
|
# Build, deploy, debug / execute a native Android package based upon |
|
# NativeActivity. |
|
|
|
declare -r script_directory=$(dirname $0) |
|
declare -r android_root=${script_directory}/../../../../../../ |
|
declare -r script_name=$(basename $0) |
|
declare -r android_manifest=AndroidManifest.xml |
|
declare -r os_name=$(uname -s) |
|
|
|
# Minimum Android target version supported by this project. |
|
: ${BUILDAPK_ANDROID_TARGET_MINVERSION:=10} |
|
# Directory containing the Android SDK |
|
# (http://developer.android.com/sdk/index.html). |
|
: ${ANDROID_SDK_HOME:=} |
|
# Directory containing the Android NDK |
|
# (http://developer.android.com/tools/sdk/ndk/index.html). |
|
: ${NDK_HOME:=} |
|
|
|
# Display script help and exit. |
|
usage() { |
|
echo " |
|
Build the Android package in the current directory and deploy it to a |
|
connected device. |
|
|
|
Usage: ${script_name} \\ |
|
[ADB_DEVICE=serial_number] [BUILD=0] [DEPLOY=0] [RUN_DEBUGGER=1] \ |
|
[LAUNCH=0] [SWIG_BIN=swig_binary_directory] [SWIG_LIB=swig_include_directory] [ndk-build arguments ...] |
|
|
|
ADB_DEVICE=serial_number: |
|
serial_number specifies the device to deploy the built apk to if multiple |
|
Android devices are connected to the host. |
|
BUILD=0: |
|
Disables the build of the package. |
|
DEPLOY=0: |
|
Disables the deployment of the built apk to the Android device. |
|
RUN_DEBUGGER=1: |
|
Launches the application in gdb after it has been deployed. To debug in |
|
gdb, NDK_DEBUG=1 must also be specified on the command line to build a |
|
debug apk. |
|
LAUNCH=0: |
|
Disable the launch of the apk on the Android device. |
|
SWIG_BIN=swig_binary_directory: |
|
The directory where the SWIG binary lives. No need to set this if SWIG is |
|
installed and point to from your PATH variable. |
|
SWIG_LIB=swig_include_directory: |
|
The directory where SWIG shared include files are, usually obtainable from |
|
commandline with \"swig -swiglib\". No need to set this if SWIG is installed |
|
and point to from your PATH variable. |
|
ndk-build arguments...: |
|
Additional arguments for ndk-build. See ndk-build -h for more information. |
|
" >&2 |
|
exit 1 |
|
} |
|
|
|
# Get the number of CPU cores present on the host. |
|
get_number_of_cores() { |
|
case ${os_name} in |
|
Darwin) |
|
sysctl hw.ncpu | awk '{ print $2 }' |
|
;; |
|
CYGWIN*|Linux) |
|
awk '/^processor/ { n=$3 } END { print n + 1 }' /proc/cpuinfo |
|
;; |
|
*) |
|
echo 1 |
|
;; |
|
esac |
|
} |
|
|
|
# Get the package name from an AndroidManifest.xml file. |
|
get_package_name_from_manifest() { |
|
xmllint --xpath 'string(/manifest/@package)' "${1}" |
|
} |
|
|
|
# Get the library name from an AndroidManifest.xml file. |
|
get_library_name_from_manifest() { |
|
echo "\ |
|
setns android=http://schemas.android.com/apk/res/android |
|
xpath string(/manifest/application/activity\ |
|
[@android:name=\"android.app.NativeActivity\"]/meta-data\ |
|
[@android:name=\"android.app.lib_name\"]/@android:value)" | |
|
xmllint --shell "${1}" | awk '/Object is a string/ { print $NF }' |
|
} |
|
|
|
# Get the number of Android devices connected to the system. |
|
get_number_of_devices_connected() { |
|
adb devices -l | \ |
|
awk '/^..*$/ { if (p) { print $0 } } |
|
/List of devices attached/ { p = 1 }' | \ |
|
wc -l |
|
return ${PIPESTATUS[0]} |
|
} |
|
|
|
# Kill a process and its' children. This is provided for cygwin which |
|
# doesn't ship with pkill. |
|
kill_process_group() { |
|
local parent_pid="${1}" |
|
local child_pid= |
|
for child_pid in $(ps -f | \ |
|
awk '{ if ($3 == '"${parent_pid}"') { print $2 } }'); do |
|
kill_process_group "${child_pid}" |
|
done |
|
kill "${parent_pid}" 2>/dev/null |
|
} |
|
|
|
# Find and run "adb". |
|
adb() { |
|
local adb_path= |
|
for path in "$(which adb 2>/dev/null)" \ |
|
"${ANDROID_SDK_HOME}/sdk/platform-tools/adb" \ |
|
"${android_root}/prebuilts/sdk/platform-tools/adb"; do |
|
if [[ -e "${path}" ]]; then |
|
adb_path="${path}" |
|
break |
|
fi |
|
done |
|
if [[ "${adb_path}" == "" ]]; then |
|
echo -e "Unable to find adb." \ |
|
"\nAdd the Android ADT sdk/platform-tools directory to the" \ |
|
"PATH." >&2 |
|
exit 1 |
|
fi |
|
"${adb_path}" "$@" |
|
} |
|
|
|
# Find and run "android". |
|
android() { |
|
local android_executable=android |
|
if echo "${os_name}" | grep -q CYGWIN; then |
|
android_executable=android.bat |
|
fi |
|
local android_path= |
|
for path in "$(which ${android_executable})" \ |
|
"${ANDROID_SDK_HOME}/sdk/tools/${android_executable}" \ |
|
"${android_root}/prebuilts/sdk/tools/${android_executable}"; do |
|
if [[ -e "${path}" ]]; then |
|
android_path="${path}" |
|
break |
|
fi |
|
done |
|
if [[ "${android_path}" == "" ]]; then |
|
echo -e "Unable to find android tool." \ |
|
"\nAdd the Android ADT sdk/tools directory to the PATH." >&2 |
|
exit 1 |
|
fi |
|
# Make sure ant is installed. |
|
if [[ "$(which ant)" == "" ]]; then |
|
echo -e "Unable to find ant." \ |
|
"\nPlease install ant and add to the PATH." >&2 |
|
exit 1 |
|
fi |
|
|
|
"${android_path}" "$@" |
|
} |
|
|
|
# Find and run "ndk-build" |
|
ndkbuild() { |
|
local ndkbuild_path= |
|
for path in "$(which ndk-build 2>/dev/null)" \ |
|
"${NDK_HOME}/ndk-build" \ |
|
"${android_root}/prebuilts/ndk/current/ndk-build"; do |
|
if [[ -e "${path}" ]]; then |
|
ndkbuild_path="${path}" |
|
break |
|
fi |
|
done |
|
if [[ "${ndkbuild_path}" == "" ]]; then |
|
echo -e "Unable to find ndk-build." \ |
|
"\nAdd the Android NDK directory to the PATH." >&2 |
|
exit 1 |
|
fi |
|
"${ndkbuild_path}" "$@" |
|
} |
|
|
|
# Get file modification time of $1 in seconds since the epoch. |
|
stat_mtime() { |
|
local filename="${1}" |
|
case ${os_name} in |
|
Darwin) stat -f%m "${filename}" 2>/dev/null || echo 0 ;; |
|
*) stat -c%Y "${filename}" 2>/dev/null || echo 0 ;; |
|
esac |
|
} |
|
|
|
# Build the native (C/C++) build targets in the current directory. |
|
build_native_targets() { |
|
# Save the list of output modules in the install directory so that it's |
|
# possible to restore their timestamps after the build is complete. This |
|
# works around a bug in ndk/build/core/setup-app.mk which results in the |
|
# unconditional execution of the clean-installed-binaries rule. |
|
restore_libraries="$(find libs -type f 2>/dev/null | \ |
|
sed -E 's@^libs/(.*)@\1@')" |
|
|
|
# Build native code. |
|
ndkbuild -j$(get_number_of_cores) "$@" |
|
|
|
# Restore installed libraries. |
|
# Obviously this is a nasty hack (along with ${restore_libraries} above) as |
|
# it assumes it knows where the NDK will be placing output files. |
|
( |
|
IFS=$'\n' |
|
for libpath in ${restore_libraries}; do |
|
source_library="obj/local/${libpath}" |
|
target_library="libs/${libpath}" |
|
if [[ -e "${source_library}" ]]; then |
|
cp -a "${source_library}" "${target_library}" |
|
fi |
|
done |
|
) |
|
} |
|
|
|
# Select the oldest installed android build target that is at least as new as |
|
# BUILDAPK_ANDROID_TARGET_MINVERSION. If a suitable build target isn't found, |
|
# this function prints an error message and exits with an error. |
|
select_android_build_target() { |
|
local -r android_targets_installed=$( \ |
|
android list targets | \ |
|
awk -F'"' '/^id:.*android/ { print $2 }') |
|
local android_build_target= |
|
for android_target in $(echo "${android_targets_installed}" | \ |
|
awk -F- '{ print $2 }' | sort -n); do |
|
local isNumber='^[0-9]+$' |
|
# skip preview API releases e.g. 'android-L' |
|
if [[ $android_target =~ $isNumber ]]; then |
|
if [[ $((android_target)) -ge \ |
|
$((BUILDAPK_ANDROID_TARGET_MINVERSION)) ]]; then |
|
android_build_target="android-${android_target}" |
|
break |
|
fi |
|
# else |
|
# The API version is a letter, so skip it. |
|
fi |
|
done |
|
if [[ "${android_build_target}" == "" ]]; then |
|
echo -e \ |
|
"Found installed Android targets:" \ |
|
"$(echo ${android_targets_installed} | sed 's/ /\n /g;s/^/\n /;')" \ |
|
"\nAndroid SDK platform" \ |
|
"android-$((BUILDAPK_ANDROID_TARGET_MINVERSION))" \ |
|
"must be installed to build this project." \ |
|
"\nUse the \"android\" application to install API" \ |
|
"$((BUILDAPK_ANDROID_TARGET_MINVERSION)) or newer." >&2 |
|
exit 1 |
|
fi |
|
echo "${android_build_target}" |
|
} |
|
|
|
# Sign unsigned apk $1 and write the result to $2 with key store file $3 and |
|
# password $4. |
|
# If a key store file $3 and password $4 aren't specified, a temporary |
|
# (60 day) key is generated and used to sign the package. |
|
sign_apk() { |
|
local unsigned_apk="${1}" |
|
local signed_apk="${2}" |
|
if [[ $(stat_mtime "${unsigned_apk}") -gt \ |
|
$(stat_mtime "${signed_apk}") ]]; then |
|
local -r key_alias=$(basename ${signed_apk} .apk) |
|
local keystore="${3}" |
|
local key_password="${4}" |
|
[[ "${keystore}" == "" ]] && keystore="${unsigned_apk}.keystore" |
|
[[ "${key_password}" == "" ]] && \ |
|
key_password="${key_alias}123456" |
|
if [[ ! -e ${keystore} ]]; then |
|
keytool -genkey -v -dname "cn=, ou=${key_alias}, o=fpl" \ |
|
-storepass ${key_password} \ |
|
-keypass ${key_password} -keystore ${keystore} \ |
|
-alias ${key_alias} -keyalg RSA -keysize 2048 -validity 60 |
|
fi |
|
cp "${unsigned_apk}" "${signed_apk}" |
|
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \ |
|
-keystore ${keystore} -storepass ${key_password} \ |
|
-keypass ${key_password} "${signed_apk}" ${key_alias} |
|
fi |
|
} |
|
|
|
# Build the apk $1 for package filename $2 in the current directory using the |
|
# ant build target $3. |
|
build_apk() { |
|
local -r output_apk="${1}" |
|
local -r package_filename="${2}" |
|
local -r ant_target="${3}" |
|
# Get the list of installed android targets and select the oldest target |
|
# that is at least as new as BUILDAPK_ANDROID_TARGET_MINVERSION. |
|
local -r android_build_target=$(select_android_build_target) |
|
[[ "${android_build_target}" == "" ]] && exit 1 |
|
echo "Building ${output_apk} for target ${android_build_target}" >&2 |
|
|
|
# Create / update build.xml and local.properties files. |
|
if [[ $(stat_mtime "${android_manifest}") -gt \ |
|
$(stat_mtime build.xml) ]]; then |
|
android update project --target "${android_build_target}" \ |
|
-n ${package_filename} --path . |
|
fi |
|
|
|
# Use ant to build the apk. |
|
ant -quiet ${ant_target} |
|
|
|
# Sign release apks with a temporary key as these packages will not be |
|
# redistributed. |
|
local unsigned_apk="bin/${package_filename}-${ant_target}-unsigned.apk" |
|
if [[ "${ant_target}" == "release" ]]; then |
|
sign_apk "${unsigned_apk}" "${output_apk}" "" "" |
|
fi |
|
} |
|
|
|
# Uninstall package $1 and install apk $2 on device $3 where $3 is "-s device" |
|
# or an empty string. If $3 is an empty string adb will fail when multiple |
|
# devices are connected to the host system. |
|
install_apk() { |
|
local -r uninstall_package_name="${1}" |
|
local -r install_apk="${2}" |
|
local -r adb_device="${3}" |
|
# Uninstall the package if it's already installed. |
|
adb ${adb_device} uninstall "${uninstall_package_name}" 1>&2 > /dev/null || \ |
|
true # no error check |
|
|
|
# Install the apk. |
|
# NOTE: The following works around adb not returning an error code when |
|
# it fails to install an apk. |
|
echo "Install ${install_apk}" >&2 |
|
local -r adb_install_result=$(adb ${adb_device} install "${install_apk}") |
|
echo "${adb_install_result}" |
|
if echo "${adb_install_result}" | grep -qF 'Failure ['; then |
|
exit 1 |
|
fi |
|
} |
|
|
|
# Launch previously installed package $1 on device $2. |
|
# If $2 is an empty string adb will fail when multiple devices are connected |
|
# to the host system. |
|
launch_package() { |
|
( |
|
# Determine the SDK version of Android on the device. |
|
local -r android_sdk_version=$( |
|
adb ${adb_device} shell cat system/build.prop | \ |
|
awk -F= '/ro.build.version.sdk/ { |
|
v=$2; sub(/[ \r\n]/, "", v); print v |
|
}') |
|
|
|
# Clear logs from previous runs. |
|
# Note that logcat does not just 'tail' the logs, it dumps the entire log |
|
# history. |
|
adb ${adb_device} logcat -c |
|
|
|
local finished_msg='Displayed '"${package_name}" |
|
local timeout_msg='Activity destroy timeout.*'"${package_name}" |
|
# Maximum time to wait before stopping log monitoring. 0 = infinity. |
|
local launch_timeout=0 |
|
# If this is a Gingerbread device, kill log monitoring after 10 seconds. |
|
if [[ $((android_sdk_version)) -le 10 ]]; then |
|
launch_timeout=10 |
|
fi |
|
# Display logcat in the background. |
|
# Stop displaying the log when the app launch / execution completes or the |
|
# logcat |
|
( |
|
adb ${adb_device} logcat | \ |
|
awk " |
|
{ |
|
print \$0 |
|
} |
|
|
|
/ActivityManager.*: ${finished_msg}/ { |
|
exit 0 |
|
} |
|
|
|
/ActivityManager.*: ${timeout_msg}/ { |
|
exit 0 |
|
}" & |
|
adb_logcat_pid=$!; |
|
if [[ $((launch_timeout)) -gt 0 ]]; then |
|
sleep $((launch_timeout)); |
|
kill ${adb_logcat_pid}; |
|
else |
|
wait ${adb_logcat_pid}; |
|
fi |
|
) & |
|
logcat_pid=$! |
|
# Kill adb logcat if this shell exits. |
|
trap "kill_process_group ${logcat_pid}" SIGINT SIGTERM EXIT |
|
|
|
# If the SDK is newer than 10, "am" supports stopping an activity. |
|
adb_stop_activity= |
|
if [[ $((android_sdk_version)) -gt 10 ]]; then |
|
adb_stop_activity=-S |
|
fi |
|
|
|
# Launch the activity and wait for it to complete. |
|
adb ${adb_device} shell am start ${adb_stop_activity} -n \ |
|
${package_name}/android.app.NativeActivity |
|
|
|
wait "${logcat_pid}" |
|
) |
|
} |
|
|
|
# See usage(). |
|
main() { |
|
# Parse arguments for this script. |
|
local adb_device= |
|
local ant_target=release |
|
local disable_deploy=0 |
|
local disable_build=0 |
|
local run_debugger=0 |
|
local launch=1 |
|
local build_package=1 |
|
for opt; do |
|
case ${opt} in |
|
# NDK_DEBUG=0 tells ndk-build to build this as debuggable but to not |
|
# modify the underlying code whereas NDK_DEBUG=1 also builds as debuggable |
|
# but does modify the code |
|
NDK_DEBUG=1) ant_target=debug ;; |
|
NDK_DEBUG=0) ant_target=debug ;; |
|
ADB_DEVICE*) adb_device="$(\ |
|
echo "${opt}" | sed -E 's/^ADB_DEVICE=([^ ]+)$/-s \1/;t;s/.*//')" ;; |
|
BUILD=0) disable_build=1 ;; |
|
DEPLOY=0) disable_deploy=1 ;; |
|
RUN_DEBUGGER=1) run_debugger=1 ;; |
|
LAUNCH=0) launch=0 ;; |
|
clean) build_package=0 disable_deploy=1 launch=0 ;; |
|
-h|--help|help) usage ;; |
|
esac |
|
done |
|
|
|
# If a target device hasn't been specified and multiple devices are connected |
|
# to the host machine, display an error. |
|
local -r devices_connected=$(get_number_of_devices_connected) |
|
if [[ "${adb_device}" == "" && $((devices_connected)) -gt 1 && \ |
|
($((disable_deploy)) -eq 0 || $((launch)) -ne 0 || \ |
|
$((run_debugger)) -ne 0) ]]; then |
|
if [[ $((disable_deploy)) -ne 0 ]]; then |
|
echo "Deployment enabled, disable using DEPLOY=0" >&2 |
|
fi |
|
if [[ $((launch)) -ne 0 ]]; then |
|
echo "Launch enabled." >&2 |
|
fi |
|
if [[ $((disable_deploy)) -eq 0 ]]; then |
|
echo "Deployment enabled." >&2 |
|
fi |
|
if [[ $((run_debugger)) -ne 0 ]]; then |
|
echo "Debugger launch enabled." >&2 |
|
fi |
|
echo " |
|
Multiple Android devices are connected to this host. Either disable deployment |
|
and execution of the built .apk using: |
|
\"${script_name} DEPLOY=0 LAUNCH=0\" |
|
|
|
or specify a device to deploy to using: |
|
\"${script_name} ADB_DEVICE=\${device_serial}\". |
|
|
|
The Android devices connected to this machine are: |
|
$(adb devices -l) |
|
" >&2 |
|
exit 1 |
|
fi |
|
|
|
if [[ $((disable_build)) -eq 0 ]]; then |
|
# Build the native target. |
|
build_native_targets "$@" |
|
fi |
|
|
|
# Get the package name from the manifest. |
|
local -r package_name=$(get_package_name_from_manifest "${android_manifest}") |
|
if [[ "${package_name}" == "" ]]; then |
|
echo -e "No package name specified in ${android_manifest},"\ |
|
"skipping apk build, deploy" |
|
"\nand launch steps." >&2 |
|
exit 0 |
|
fi |
|
local -r package_basename=${package_name/*./} |
|
local package_filename=$(get_library_name_from_manifest ${android_manifest}) |
|
[[ "${package_filename}" == "" ]] && package_filename="${package_basename}" |
|
|
|
# Output apk name. |
|
local -r output_apk="bin/${package_filename}-${ant_target}.apk" |
|
|
|
if [[ $((disable_build)) -eq 0 && $((build_package)) -eq 1 ]]; then |
|
# Build the apk. |
|
build_apk "${output_apk}" "${package_filename}" "${ant_target}" |
|
fi |
|
|
|
# Deploy to the device. |
|
if [[ $((disable_deploy)) -eq 0 ]]; then |
|
install_apk "${package_name}" "${output_apk}" "${adb_device}" |
|
fi |
|
|
|
if [[ "${ant_target}" == "debug" && $((run_debugger)) -eq 1 ]]; then |
|
# Start debugging. |
|
ndk-gdb ${adb_device} --start |
|
elif [[ $((launch)) -eq 1 ]]; then |
|
launch_package "${package_name}" "${adb_device}" |
|
fi |
|
} |
|
|
|
main "$@"
|
|
|