blob: 1b9f4a45143bec83ff25544e7af74caac5832780 [file] [log] [blame]
#!/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 "$@"