#!/bin/sh # Copyright (c) 2012 Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Sanitize the environment export LANG=C export LC_ALL=C if [ "$BASH_VERSION" ]; then set -o posix fi PROGDIR=$(dirname "$0") PROGDIR=$(cd "$PROGDIR" && pwd) PROGNAME=$(basename "$0") # Utility functions TMPDIR= # Used to exit the program after removing the temporary directory. clean_exit () { if [ "$TMPDIR" ]; then if [ -z "$NO_CLEANUP" ]; then log "Cleaning up: $TMPDIR" rm -rf "$TMPDIR" else dump "Temporary directory contents preserved: $TMPDIR" fi fi exit "$@" } # Dump a panic message then exit. panic () { echo "ERROR: $@" clean_exit 1; } # If the previous command failed, dump a panic message then exit. fail_panic () { if [ $? != 0 ]; then panic "$@" fi; } # Extract number of cores to speed up the builds get_core_count () { case $(uname -s) in Linux) grep -c -e '^processor' /proc/cpuinfo ;; Darwin) sysctl -n hw.ncpu ;; CYGWIN*|*_NT-*) echo $NUMBER_OF_PROCESSORS ;; *) echo 1 ;; esac } DEFAULT_ABI="armeabi" VALID_ABIS="armeabi armeabi-v7a x86 mips" ABI= ADB= ENABLE_M32= HELP= HELP_ALL= NDK_DIR= NO_CLEANUP= NO_DEVICE= NUM_JOBS=$(get_core_count) TMPDIR= VERBOSE=0 for opt do # The following extracts the value if the option is like --name=. optarg=$(expr -- $opt : '^--[^=]*=\(.*\)$') case $opt in --abi=*) ABI=$optarg;; --adb=*) ADB=$optarg;; --enable-m32) ENABLE_M32=true;; --help|-h|-?) HELP=TRUE;; --help-all) HELP_ALL=true;; --jobs=*) NUM_JOBS=$optarg;; --ndk-dir=*) NDK_DIR=$optarg;; --tmp-dir=*) TMPDIR=$optarg;; --no-cleanup) NO_CLEANUP=true;; --no-device) NO_DEVICE=true;; --quiet) VERBOSE=$(( $VERBOSE - 1 ));; --verbose) VERBOSE=$(( $VERBOSE + 1 ));; -*) panic "Invalid option '$opt', see --help for details.";; *) panic "This script doesn't take any parameters. See --help for details." ;; esac done if [ "$HELP" -o "$HELP_ALL" ]; then echo "\ Usage: $PROGNAME [options] This script is used to check that your Google Breakpad source tree can be properly built for Android, and that the client library and host tools work properly together. " if [ "$HELP_ALL" ]; then echo "\ In more details, this script will: - Rebuild the host version of Google Breakpad in a temporary directory (with the Auto-tools based build system). - Rebuild the Android client library with the Google Breakpad build system (using autotools/configure). This requires that you define ANDROID_NDK_ROOT in your environment to point to a valid Android NDK installation directory, or use the --ndk-dir= option. - Rebuild the Android client library and a test crashing program with the Android NDK build system (ndk-build). - Require an Android device connected to your machine, and the 'adb' tool in your path. They are used to: - Install and run a test crashing program. - Extract the corresponding minidump from the device. - Dump the symbols from the test program on the host with 'dump_syms' - Generate a stack trace with 'minidump_stackwalk' - Check the stack trace content for valid source file locations. You can however skip this requirement and only test the builds by using the --no-device flag. By default, all generated files will be created in a temporary directory that is removed when the script completion. If you want to inspect the files, use the --no-cleanup option. Finally, use --verbose to increase the verbosity level, this will help you see which exact commands are being issues and their result. Use the flag twice for even more output. Use --quiet to decrease verbosity instead and run the script silently. If you have a device connected, the script will probe it to determine its primary CPU ABI, and build the test program for it. You can however use the --abi= option to override this (this can be useful to check the secondary ABI, e.g. using --abi=armeabi to check that such a program works correctly on an ARMv7-A device). If you don't have a device connected, the test program will be built (but not run) with the default '$DEFAULT_ABI' ABI. Again, you can use --abi= to override this. Valid ABI names are: $VALID_ABIS " fi # HELP_ALL echo "\ Valid options: --help|-h|-? Display this message. --help-all Display extended help. --enable-m32 Build 32-bit version of host tools. --abi= Specify target CPU ABI [auto-detected]. --jobs= Run build tasks in parallel [$NUM_JOBS]. --ndk-dir= Specify NDK installation directory. --tmp-dir= Specify temporary directory (will be wiped-out). --adb= Specify adb program path. --no-cleanup Don't remove temporary directory after completion. --no-device Do not try to detect devices, nor run crash test. --verbose Increase verbosity. --quiet Decrease verbosity." clean_exit 0 fi # Dump message to stdout, unless verbosity is < 0, i.e. --quiet was called dump () { if [ "$VERBOSE" -ge 0 ]; then echo "$@" fi } # If --verbose was used, dump a message to stdout. log () { if [ "$VERBOSE" -ge 1 ]; then echo "$@" fi } # Run a command. Output depends on $VERBOSE: # $VERBOSE <= 0: Run command, store output into the run log # $VERBOSE >= 1: Dump command, run it, output goest to stdout # Note: Ideally, the command's output would go to the run log for $VERBOSE >= 1 # but the 'tee' tool doesn't preserve the status code of its input pipe # in case of error. run () { local LOGILE if [ "$RUN_LOG" ]; then LOGFILE=$RUN_LOG else LOGFILE=/dev/null fi if [ "$VERBOSE" -ge 1 ]; then echo "COMMAND: $@" "$@" else "$@" >>$LOGFILE 2>&1 fi } # Same as run(), but only dump command output for $VERBOSE >= 2 run2 () { local LOGILE if [ "$RUN_LOG" ]; then LOGFILE=$RUN_LOG else LOGFILE=/dev/null fi if [ "$VERBOSE" -ge 1 ]; then echo "COMMAND: $@" fi if [ "$VERBOSE" -ge 2 ]; then "$@" else "$@" >>$LOGFILE 2>&1 fi } TESTAPP_DIR=$PROGDIR/sample_app # Select NDK install directory. if [ -z "$NDK_DIR" ]; then if [ -z "$ANDROID_NDK_ROOT" ]; then panic "Please define ANDROID_NDK_ROOT in your environment, or use \ --ndk-dir=." fi NDK_DIR="$ANDROID_NDK_ROOT" log "Found NDK directory: $NDK_DIR" else log "Using NDK directory: $NDK_DIR" fi # Small sanity check. NDK_BUILD="$NDK_DIR/ndk-build" if [ ! -f "$NDK_BUILD" ]; then panic "Your NDK directory is not valid (missing ndk-build): $NDK_DIR" fi # If --tmp-dir= is not used, create a temporary directory. # Otherwise, start by cleaning up the user-provided path. if [ -z "$TMPDIR" ]; then TMPDIR=$(mktemp -d /tmp/$PROGNAME.XXXXXXXX) fail_panic "Can't create temporary directory!" log "Using temporary directory: $TMPDIR" else if [ ! -d "$TMPDIR" ]; then mkdir -p "$TMPDIR" fail_panic "Can't create temporary directory: $TMPDIR" else log "Cleaning up temporary directory: $TMPDIR" rm -rf "$TMPDIR"/* fail_panic "Cannot cleanup temporary directory!" fi fi # Ensure a clean exit when the script is: # - Interrupted by Ctrl-C (INT) # - Interrupted by log out (HUP) # - Being asked to quit nicely (TERM) # - Being asked to quit and dump core (QUIT) trap "clean_exit 1" INT HUP TERM QUIT # The 'adb shell' command is pretty hopeless, try to make sense of it by: # 1/ Removing trailing \r from line endings. # 2/ Ensuring the function returns the command's status code. # adb_shell () { local RET ADB_LOG ADB_LOG=$(mktemp "$TMPDIR/adb-XXXXXXXX") "$ADB" shell "$@" ";" echo \$? > "$ADB_LOG" 2>&1 sed -i -e 's![[:cntrl:]]!!g' "$ADB_LOG" # Remove \r. RET=$(sed -e '$!d' "$ADB_LOG") # Last line contains status code. sed -e '$d' "$ADB_LOG" # Print everything except last line. rm -f "$ADB_LOG" return $RET } check_for_adb () { local ADB_VERSION ADB_DEVICES NUM_DEVICES FINGERPRINT # Auto-detect ADB in current path when needed. if [ -z "$ADB" ]; then ADB=$(which adb 2>/dev/null) if [ -z "$ADB" ]; then panic "The 'adb' tool is not in your path! Use either --no-device to\ only check the builds, or --adb= to specify the tool path." fi log "Found ADB path: $ADB" else log "Using ADB path: $ADB" fi # Check that it works. ADB_VERSION=$("$ADB" version 2>/dev/null) case $ADB_VERSION in "Android Debug Bridge "*) # Pass. log "Found ADB version: $ADB_VERSION" ;; *) # Fail. panic "Your ADB binary does not seem to work: $ADB" ;; esac # Count the number of connected devices. ADB_DEVICES=$("$ADB" devices 2>/dev/null | awk '$2 == "device" { print $1; }') if [ "$ADB_DEVICES" ]; then NUM_DEVICES=$(echo "$ADB_DEVICES" | wc -l) else NUM_DEVICES=0 fi case $NUM_DEVICES in 0) panic "No Android device connected! Connect one, or use --no-device \ to only check the builds." ;; 1) export ANDROID_SERIAL=$ADB_DEVICES ;; *) if [ "$ANDROID_SERIAL" ]; then ADB_DEVICES=$ANDROID_SERIAL NUM_DEVICES=1 else dump "ERROR: More than one Android device connected. Please define \ ANDROID_SERIAL" dump " in your environment, or use --no-device to only check \ the builds." clean_exit 1 fi ;; esac FINGERPRINT=$(adb_shell getprop ro.build.fingerprint) dump "Using device: $ANDROID_SERIAL ($FINGERPRINT)" } if [ -z "$NO_DEVICE" ]; then check_for_adb fi BUILD_LOG="$TMPDIR/build.log" RUN_LOG="$TMPDIR/run.log" CRASH_LOG="$TMPDIR/crash.log" TMPHOST="$TMPDIR/host-local" cd "$TMPDIR" # Build host version of the tools dump "Building host tools." CONFIGURE_FLAGS= if [ "$ENABLE_M32" ]; then CONFIGURE_FLAGS="$CONFIGURE_FLAGS --enable-m32" fi ( run mkdir "$TMPDIR/build-host" && run cd "$TMPDIR/build-host" && run2 "$PROGDIR/../configure" --prefix="$TMPHOST" $CONFIGURE_FLAGS && run2 make -j$NUM_JOBS install ) fail_panic "Can't build host-tools!" TMPBIN=$TMPHOST/bin # Generate a stand-alone NDK toolchain # Extract CPU ABI and architecture from device, if any. if [ "$ADB" ]; then DEVICE_ABI=$(adb_shell getprop ro.product.cpu.abi) DEVICE_ABI2=$(adb_shell getprop ro.product.cpu.abi2) if [ -z "$DEVICE_ABI" ]; then panic "Can't extract ABI from connected device!" fi if [ "$DEVICE_ABI2" ]; then dump "Found device ABIs: $DEVICE_ABI $DEVICE_ABI2" else dump "Found device ABI: $DEVICE_ABI" DEVICE_ABI2=$DEVICE_ABI fi # If --abi= is used, check that the device supports it. if [ "$ABI" -a "$DEVICE_ABI" != "$ABI" -a "$DEVICE_ABI2" != "$ABI" ]; then dump "ERROR: Device ABI(s) do not match --abi command-line value ($ABI)!" panic "Please use --no-device to skip device tests." fi if [ -z "$ABI" ]; then ABI=$DEVICE_ABI dump "Using CPU ABI: $ABI (device)" else dump "Using CPU ABI: $ABI (command-line)" fi else if [ -z "$ABI" ]; then # No device connected, choose default ABI ABI=$DEFAULT_ABI dump "Using CPU ABI: $ABI (default)" else dump "Using CPU ABI: $ABI (command-line)" fi fi # Check the ABI value VALID= for VALID_ABI in $VALID_ABIS; do if [ "$ABI" = "$VALID_ABI" ]; then VALID=true break fi done if [ -z "$VALID" ]; then panic "Unknown CPU ABI '$ABI'. Valid values are: $VALID_ABIS" fi # Extract architecture name from ABI case $ABI in armeabi*) ARCH=arm;; *) ARCH=$ABI;; esac # Extract GNU configuration name case $ARCH in arm) GNU_CONFIG=arm-linux-androideabi;; *) GNU_CONFIG="$ARCH-linux-android";; esac # Generate standalone NDK toolchain installation NDK_STANDALONE="$TMPDIR/ndk-$ARCH-toolchain" echo "Generating NDK standalone toolchain installation" mkdir -p "$NDK_STANDALONE" run "$NDK_DIR/build/tools/make-standalone-toolchain.sh" \ --arch="$ARCH" \ --install-dir="$NDK_STANDALONE" fail_panic "Can't generate standalone NDK toolchain installation!" # Rebuild the client library with the auto-tools base build system. # Even though it's not going to be used, this checks that this still # works correctly. echo "Building client Android library with configure/make" TMPTARGET="$TMPDIR/target-local" ( PATH="$NDK_STANDALONE/bin:$PATH" run mkdir "$TMPTARGET" && run mkdir "$TMPDIR"/build-target && run cd "$TMPDIR"/build-target && run2 "$PROGDIR"/../configure --prefix="$TMPTARGET" \ --host="$GNU_CONFIG" \ --disable-tools \ --disable-processor && run2 make -j$NUM_JOBS install ) fail_panic "Could not rebuild Android client library!" # Copy sources to temporary directory PROJECT_DIR=$TMPDIR/project dump "Copying test program sources to: $PROJECT_DIR" run cp -r "$TESTAPP_DIR" "$PROJECT_DIR" && run rm -rf "$PROJECT_DIR/obj" && run rm -rf "$PROJECT_DIR/libs" fail_panic "Could not copy test program sources to: $PROJECT_DIR" # Build the test program with ndk-build. dump "Building test program with ndk-build" export NDK_MODULE_PATH="$PROGDIR" NDK_BUILD_FLAGS="-j$NUM_JOBS" if [ "$VERBOSE" -ge 2 ]; then NDK_BUILD_FLAGS="$NDK_BUILD_FLAGS NDK_LOG=1 V=1" fi run "$NDK_DIR/ndk-build" -C "$PROJECT_DIR" $NDK_BUILD_FLAGS APP_ABI=$ABI fail_panic "Can't build test program!" # Unless --no-device was used, stop right here if ADB isn't in the path, # or there is no connected device. if [ "$NO_DEVICE" ]; then dump "Done. Please connect a device to run all tests!" clean_exit 0 fi # Push the program to the device. TESTAPP=test_google_breakpad TESTAPP_FILE="$PROJECT_DIR/libs/$ABI/test_google_breakpad" if [ ! -f "$TESTAPP_FILE" ]; then panic "Device requires '$ABI' binaries. None found!" fi # Run the program there dump "Installing test program on device" DEVICE_TMP=/data/local/tmp run "$ADB" push "$TESTAPP_FILE" "$DEVICE_TMP/" fail_panic "Cannot push test program to device!" dump "Running test program on device" adb_shell cd "$DEVICE_TMP" "&&" ./$TESTAPP > "$CRASH_LOG" 2>/dev/null if [ $? = 0 ]; then panic "Test program did *not* crash as expected!" fi if [ "$VERBOSE" -ge 1 ]; then echo -n "Crash log: " cat "$CRASH_LOG" fi # Extract minidump from device MINIDUMP_NAME=$(awk '$1 == "Dump" && $2 == "path:" { print $3; }' "$CRASH_LOG") MINIDUMP_NAME=$(basename "$MINIDUMP_NAME") if [ -z "$MINIDUMP_NAME" ]; then panic "Test program didn't write minidump properly!" fi dump "Extracting minidump: $MINIDUMP_NAME" run "$ADB" pull "$DEVICE_TMP/$MINIDUMP_NAME" . fail_panic "Can't extract minidump!" dump "Parsing test program symbols" if [ "$VERBOSE" -ge 1 ]; then log "COMMAND: $TMPBIN/dump_syms \ $PROJECT_DIR/obj/local/$ABI/$TESTAPP >$TESTAPP.sym" fi "$TMPBIN/dump_syms" "$PROJECT_DIR/obj/local/$ABI/$TESTAPP" > $TESTAPP.sym fail_panic "dump_syms doesn't work!" VERSION=$(awk '$1 == "MODULE" { print $4; }' $TESTAPP.sym) dump "Found module version: $VERSION" if [ -z "$VERSION" ]; then echo "ERROR: Can't find proper module version from symbol dump!" head -n5 $TESTAPP.sym clean_exit 1 fi run mkdir -p "$TMPDIR/symbols/$TESTAPP/$VERSION" run mv $TESTAPP.sym "$TMPDIR/symbols/$TESTAPP/$VERSION/" dump "Generating stack trace" # Don't use 'run' to be able to send stdout and stderr to two different files. log "COMMAND: $TMPBIN/minidump_stackwalk $MINIDUMP_NAME symbols" "$TMPBIN/minidump_stackwalk" $MINIDUMP_NAME \ "$TMPDIR/symbols" \ > "$BUILD_LOG" 2>>"$RUN_LOG" fail_panic "minidump_stackwalk doesn't work!" dump "Checking stack trace content" if [ "$VERBOSE" -ge 1 ]; then cat "$BUILD_LOG" fi # The generated stack trace should look like the following: # # Thread 0 (crashed) # 0 test_google_breakpad!crash [test_breakpad.cpp : 17 + 0x4] # r4 = 0x00015530 r5 = 0xbea2cbe4 r6 = 0xffffff38 r7 = 0xbea2cb5c # r8 = 0x00000000 r9 = 0x00000000 r10 = 0x00000000 fp = 0x00000000 # sp = 0xbea2cb50 lr = 0x00009025 pc = 0x00008f84 # Found by: given as instruction pointer in context # 1 test_google_breakpad!main [test_breakpad.cpp : 25 + 0x3] # r4 = 0x00015530 r5 = 0xbea2cbe4 r6 = 0xffffff38 r7 = 0xbea2cb5c # r8 = 0x00000000 r9 = 0x00000000 r10 = 0x00000000 fp = 0x00000000 # sp = 0xbea2cb50 pc = 0x00009025 # Found by: call frame info # 2 libc.so + 0x164e5 # r4 = 0x00008f64 r5 = 0xbea2cc34 r6 = 0x00000001 r7 = 0xbea2cc3c # r8 = 0x00000000 r9 = 0x00000000 r10 = 0x00000000 fp = 0x00000000 # sp = 0xbea2cc18 pc = 0x400c34e7 # Found by: call frame info # ... # # The most important part for us is ensuring that the source location could # be extracted, so look at the 'test_breakpad.cpp' references here. # # First, extract all the lines with test_google_breakpad! in them, and # dump the corresponding crash location. # # Note that if the source location can't be extracted, the second field # will only be 'test_google_breakpad' without the exclamation mark. # LOCATIONS=$(awk '$2 ~ "^test_google_breakpad!.*" { print $3; }' "$BUILD_LOG") if [ -z "$LOCATIONS" ]; then if [ "$VERBOSE" -lt 1 ]; then cat "$BUILD_LOG" fi panic "No source location found in stack trace!" fi # Now check that they all match "[" BAD_LOCATIONS= for LOCATION in $LOCATIONS; do case $LOCATION in # Escape the opening bracket, or some shells like Dash will not # match them properly. \[*.cpp|\[*.cc|\[*.h) # These are valid source locations in our executable ;; *) # Everything else is not! BAD_LOCATIONS="$BAD_LOCATIONS $LOCATION" ;; esac done if [ "$BAD_LOCATIONS" ]; then dump "ERROR: Generated stack trace doesn't contain valid source locations:" cat "$BUILD_LOG" echo "Bad locations are: $BAD_LOCATIONS" clean_exit 1 fi echo "All clear! Congratulations." clean_exit 0