#!/bin/bash

# This is a script to built "fat" ARM binaries that contain two separate
# sets of executable code, one using the soft-float API and one using
# the hard-float API.
#
# Copyright (c) Red Hat 2011
# Written by: Nick Clifton  <nickc@redhat.com>
# Released under the LGPL v3.  See: http://www.gnu.org/licenses/lgpl.html

# For a description of the command line options supported by this script
# see the help() function.
#
# Note: if you are modifying this script to run in a new enviroment then
# you should only need to change the contents of the init_user() function.
#
# Note: the intention is that this script should be named "gcc" and
# placed in the execution path ahead of the real gcc.  Then it can
# be used inside rpm build scripts and makefiles.
#
# Note: The latest version of this script can be found here:
#  FIXME: FILL IN
#
#----------------------------------------------------------------

# Initialise user modifiable global variables.
init_user ()
{
    # The command line switch(es) to add to the end of a compilation
    # in order to generate the alternative API:
    ALT_API="-mfloat-abi=hard"

    # The name of the section that is inserted into a binary that
    # contains the alternative API code:
    ALT_SEC=".note.alternative-api"

    # The exec base for the tools to use.  If this is initialised
    # and any of the program variables are not initialised
    # then they will be automatically filled in based on this variable.
    EXEC_BASE=/work/builds/gcc/current/arm-eabi/install/bin/arm-eabi

    # The compiler to use:
    GCC=${EXEC_BASE}-gcc # This is an example of how this variable would
                         # be automatically initialised.

    # The other program variables are:
    # OBJCOPY=
    # OBJDUMP=
    # READELF=
}

#----------------------------------------------------------------

help ()
{
  # The following exec goop is so that we don't have to manually
  # redirect every message to stderr in this function.
    exec 4>&1    # save stdout fd to fd #4
    exec 1>&2    # redirect stdout to stderr

    cat <<__EOM__
  This is a shell script that acts as a wrapper to gcc and which allows the
  building of "fat binaries".  These are files that contain code that supports
  two different APIs.  The normal API code is visible to other tools and the
  kernel, whilst the alternative API code is stored hidden inside the binary.
  The --fat-swap option of this script can be used to swap the two APIs over.

Usage: `basename $0` {fat-options} {fat-command} {gcc-arguments} {file-names}

{fat-option} is one or more of:

  --fat-version
    Report the version of this script, plus the user-customisable variables.

  --fat-help
    Displays this information.

  --fat-verbose
    This tells the script to be verbose about what it is doing.  The same
    effect can achieved by using gcc's --verbose command line option,
    except that that makes gcc generate verbose output as well.

  --fat-save-temps
    Do not delete any temporary files created by the script.

{fat-command} is one of the following:

  --fat-status [foo]
    Reports on the status of the specified files, showing which API is in
    use and which is stored internally.

  --fat-swap [foo]
    Swaps the contents of the specified files so that the stored
    alternative version is the one that will be seen by other tools.

  [gcc-arguments] -c foo.c
    Compiles foo.c with the given set of command line arguments.
    Then recompiles foo.c with this option appended:
      $ALT_API
    The resulting object file is stored inside foo.o in a section called:
      $ALT_SEC

  [linker-arguments] foo.o -o foo
    Links foo.o, creating the executable foo.
    Then relinks foo.o using the alternate API version of the object file
    and all of the libraries involved.  Stores the resulting new executable
    back inside foo.
__EOM__

    exec 1>&4   # Copy stdout fd back from temporary save fd, #4
}

#----------------------------------------------------------------

report ()
{
  if [ $SHOW_PROGRESS -ne 0 ]
  then
    echo $prog": " ${1+"$@"}
  fi
}

verbose ()
{
  if [ $SHOW_PROGRESS -gt 1 ] || [ $SHOW_PROGRESS -lt -1 ]
  then
    report ${1+"$@"}
  fi
}

warn ()
{
    if test "x$1" != "x" ;
    then
	report "Warning: $1"
    fi
}

fail ()
{
    echo "$prog: Internal error: " ${1+"$@"}
    exit 1
}

verbose ()
{
    if [ $SHOW_PROGRESS -gt 1 ] || [ $SHOW_PROGRESS -lt -1 ]
    then
	report ${1+"$@"}
    fi
}

init_internal ()
{
    # The version of this script:
    VERSION=0.2

    # The name that was used to invoke this script.
    prog=`basename $0` ;
    # FIXME: If the $prog is "gcc" then maybe an alternative name should be used ?

    # Multi-state variable:
    # -2 => Do not do anything, be verbose about what would be done.
    # -1 => Do not do anything, just show what would be done.
    #  0 => Do not report anything, just return an exit code.
    #  1 => Provide a basic desription of what is going on.
    #  2 => Be verbose about what is going on.
    SHOW_PROGRESS=1

    # Set to 1 if temporary files should not be deleted.
    save_temps=0
    
    # Fill in the executable variables, if necessary.
    if test "x$GCC" == "x" ;
    then
	if test "x$EXEC_BASE" == "x" ;
	then
	    fail "No GCC executable specified and no base name either."
	else
	    GCC=${EXEC_BASE}-gcc
	fi
    fi

    if test "x$OBJCOPY" == "x" ;
    then
	if test "x$EXEC_BASE" == "x" ;
	then
	    fail "No OBJCOPY executable specified and no base name either."
	else
	    OBJCOPY=${EXEC_BASE}-objcopy
	fi
    fi

    if test "x$OBJDUMP" == "x" ;
    then
	if test "x$EXEC_BASE" == "x" ;
	then
	    fail "No OBJDUMP executable specified and no base name either."
	else
	    OBJDUMP=${EXEC_BASE}-objdump
	fi
    fi

    if test "x$READELF" == "x" ;
    then
	if test "x$EXEC_BASE" == "x" ;
	then
	    fail "No READELF executable specified and no base name either."
	else
	    READELF=${EXEC_BASE}-readelf
	fi
    fi
}

version ()
{
    report "Fat Binary building script"
    report "Version: $VERSION"
    report "Copyright (c) 2011 Red Hat"
    report " "
    report "GCC executable: $GCC"
    report "Objcopy executable: $OBJCOPY"
    report "Objdump executable: $OBJDUMP"
    report "Readelf executable: $READELF"
    report "Alternative API option: $ALT_API"
    report "Alternative API section: $ALT_SEC"
}

#----------------------------------------------------------------

delete_file ()
{
    if [ $save_temps -eq 0 ]
    then
	verbose "Deleting file: $1"

	if [ $SHOW_PROGRESS -ge 0 ]
	then
	    rm $1
	fi
    fi
}

# syntax: run <command> [<args>]
#  If being verbose report the command being run, and
#   the directory in which it is run.
#  If not performing a dry run execute the command and
#   issue a warning message if the command fails.
# Sets RUN_RESULT to 0 upon success, 1 otherwise.
run ()
{
    RUN_RESULT=0
    verbose "Executing: $*"

    if [ $SHOW_PROGRESS -lt 0 ]
    then
	return
    fi

    ( $* ) && return

    verbose "Problems encountered whilst executing: $*"
    RUN_RESULT=1
}

# Determine which API is in use in a given file.
# Written as separate function in order to make it easier to port
# this script to other architectures.
uses_hard_api ()
{
    verbose "Running: $READELF --arch-specific $1 | grep --quiet Tag_ABI_HardFP_use"
    return `$READELF --arch-specific $1 | grep --quiet Tag_ABI_HardFP_use`
}

# Switch the API on the specified file.
swap_file ()
{
    if test "x$1" = "x" ;
    then
	fail "swap_file needs a filename argument"
    fi
    if test "x$2" != "x" ;
    then
	fail "swap_file only takes one argument"
    fi

    verbose "Swapping APIs in: `basename $1`"
    if `$OBJDUMP --section-headers $1 | grep --quiet $ALT_SEC`
    then
	local alt_api_file
	local stripped_orig_file
	local file_name=`basename $1`
	
	alt_api_file=`mktemp alt-XXXXXXXXXX-$file_name`
	stripped_orig_file=`mktemp orig-XXXXXXXXXX-$file_name`
	
	# Extract the alternative API section into a separate file.
	run $OBJCOPY \
	    --only-section $ALT_SEC \
	    --output-target=binary \
	    --set-section-flags $ALT_SEC=alloc,contents,load \
	    $1 $alt_api_file
	if [ $RUN_RESULT -ne 0 ]
	then
	    fail "could not extract alternative API code"
	fi

	if `$OBJDUMP --section-headers $alt_api_file | grep --quiet $ALT_SEC`
	then
	    fail "extracted alternative API file already contains another API section!"
	fi
	
	# Remove the alternative API section from the original file.
	run $OBJCOPY --remove-section $ALT_SEC $1 $stripped_orig_file
	if [ $RUN_RESULT -ne 0 ]
	then
	    fail "could not strip out the alternative API code"
	fi

	# Insert the original file as an alternative API section into the temporary file.
	run $OBJCOPY --add-section $ALT_SEC=$stripped_orig_file $alt_api_file
	if [ $RUN_RESULT -ne 0 ]
	then
	    fail "Could not insert the alternative API code"
	fi

	# Replace the original file with the temporary file.
	mv $1 $1.tmp
	mv $alt_api_file $1
	delete_file $1.tmp

	# Tidy up.
	delete_file $stripped_orig_file
    else
	if [ $warn_no_swap -eq 1 ]
	then
	    verbose "`basename $1`: does not contain an alternative API binary"
	fi
    fi
}

# Switch over the APIs in use in the given files.
swap_files ()
{
    if test "x$1" = "x" ;
    then
	warn "no file names specified for --fat-swap option"
    fi

    warn_no_swap=1

    while [ $# -gt 0 ]
    do
	if [ -r $1 ]
	then
	    swap_file $1
	else
	    warn "Unable to read file: $1"
	fi
    shift
  done
}

# Search through the library_paths array for $1
# Sets found_file to the result.
search_for ()
{
    local i

    found_file=""

    if test "x$1" = "x" ;
    then
	fail "search_for needs a filename argument"
    fi
    if test "x$2" != "x" ;
    then
	fail "search_for only takes one argument"
    fi

    verbose "Searching for: $1"
    i=0
    while [ $i -lt ${#library_search_paths[*]} ]
    do
	local trial_path=${library_search_paths[$i]}/$1
	verbose "Testing: $trial_path"
	if [ -r $trial_path ]
	then
	    found_file=$trial_path
	    verbose "Found: $found_file"
	    return
	fi
    done

    warn "Failed to find: $1"
}

perform_link ()
{
    local gcc_output
    local -a collect_command
    local -i i
    
    if test "x$output_file" == "x" ;
    then
	output_file="a.out"
    fi
    
    if test "x$source_file" != "x" ;
    then
	# FIXME: We should allow compilation and linking in one command line.
	fail "Source file found when object files were expected"
    fi

    if [ ${#object_files[*]} -eq 0 ]
    then
	fail "No object files found"
    fi

    gcc_output=`mktemp gcc-XXXXXXXXXX-$output_file`

    # We add -v to the gcc command line so that we can recover the linker invocation.
    # Note - we do not use the run() function because we are redirecting the output.
    verbose "Running: $GCC ${1+"$@"}"
    $GCC ${1+"$@"} -v > $gcc_output 2>&1
    if [ $? -ne 0 ]
    then
	warn "Alternative executable not created because original link failed:"
	$GCC ${1+"$@"} 
	delete_file $gcc_output
	return
    fi

    # Find the collect2 command inside gcc's verbose output and assign
    # it to the collect_command array, word splitting as we go.
    read -a collect_command <<<`grep collect2 $gcc_output`
    if test "x${collect_command[0]}" == "x" ;
    then
	verbose "gcc's output: `cat $gcc_output`"
	delete_file $gcc_output
	fail "GCC did not invoke collect2"
    fi
    
    delete_file $gcc_output

    warn_no_swap=1
    declare -a library_search_paths

    verbose "Add library search path: ."
    library_search_paths=(".")

    verbose "Swapping libraries and object files"
    # Now for the tricky bit - we have to find all of the libraries and object
    # files that are involved in the linking of $output_file and switch them
    # over to their fat alternatives.
    i=1
    while [ $i -lt ${#collect_command[*]} ]
    do
	local option=${collect_command[$i]}
	case "$option" in
	    *.o)
		if test "${option:0:1}" == "/" ;
		then
		    swap_file $option
		else
		    search_for $option
		    if test "x$found_file" != "x" ;
		    then
			swap_file $found_file
		    fi
		fi
		;;
	    *.a | *.so)
		search_for $option
		if test "x$found_file" != "x" ;
		then
		    swap_library $found_file
		fi
		;;
	    -L* | -B*)
		verbose "Add library search path: ${option:2}"
		library_search_paths+=(${option:2})
		;;
	    *)
		verbose "Ignoring linker command line option: $option"
		;;
	esac
	i+=1
    done

    # Rerun the gcc command line with the swapped binaries in place
    # but creating a new output file.
    new_output_file=`mktemp aout-XXXXXXXXXX-$output_file`
    run $GCC ${1+"$@"} $ALT_API -o $new_output_file
    if [ $RUN_RESULT -ne 0 ]
    then
	fail "Alternative link failed"
    fi

    # Remove the alternative API section from the original file.
    run $OBJCOPY --remove-section $ALT_SEC $output_file
    if [ $RUN_RESULT -ne 0 ]
    then
	warn "original output file does not contain an alternative API code"
    fi
    # And from the alternative output file
    run $OBJCOPY --remove-section $ALT_SEC $new_output_file
    if [ $RUN_RESULT -ne 0 ]
    then
	warn "alternative output file does not contain alternative API code"
    fi

    # Insert the new output file into the original output file.
    run $OBJCOPY --add-section $ALT_SEC=$new_output_file $output_file
    if [ $RUN_RESULT -ne 0 ]
    then
	fail "Unable to store alternative API code inside original binary"
    fi

    delete_file $new_output_file

    verbose "Restoring swapped binaries"
    i=1
    while [ $i -lt ${#collect_command[*]} ]
    do
	local option=${collect_command[$i]}
	case "$option" in
	    *.o)
		if test "${option:0:1}" == "/" ;
		then
		    swap_file $option
		else
		    search_for $option
		    if test "x$found_file" != "x" ;
		    then
			swap_file $found_file
		    fi
		fi
		;;
	    *.a | *.so)
		search_for $option
		if test "x$found_file" != "x" ;
		then
		    swap_library $found_file
		fi
		;;
	esac
	i+=1
    done
}

# Compile a source file to an object file.
perform_compile ()
{
    local temp_file
    
    if test "x$source_file" == "x" ;
    then
	fail "Source file name not found"
    fi

    if test "x$output_file" == "x" ;
    then
	output_file="`echo $source_file | sed 's,.c,.o,'`"
    fi
    
    run $GCC ${1+"$@"}
    if [ $RUN_RESULT -ne 0 ]
    then
	verbose "Alternative compilation not run because original compilation failed."
	return
    fi

    temp_file=`mktemp fat-XXXXXXXXXX-$output_file`
    run $GCC ${1+"$@"} $ALT_API "-o $temp_file"
    if [ $RUN_RESULT -ne 0 ]
    then
	fail "Alternative compilation failed"
    fi

    # FIXME: As an optimization we could check here to see if the
    # fat binary is essentially identical to the thin binary.  If so
    # then there is no need to store the fat binary inside the thin one.

    # Store the fat version inside the thin version
    run $OBJCOPY --add-section $ALT_SEC=$temp_file $output_file
    if [ $RUN_RESULT -ne 0 ]
    then
	fail "Unable to store alternative API code inside original binary"
    fi

    delete_file $temp_file
}

# Scans a list of files, reporting on their API status.
show_status ()
{
    if test "x$1" = "x" ;
    then
	warn "no file names specified for --fat-status option"
    fi

    while [ $# -gt 0 ]
    do
	if [ -r $1 ]
	then
	    verbose "Running: $OBJDUMP --section-headers $1 | grep --quiet $ALT_SEC"
	    if `$OBJDUMP --section-headers $1 | grep --quiet $ALT_SEC`
	    then
		if uses_hard_api $1
		then
		    report "$1: uses the hard API, contains a soft API alternative"
		else
		    report "$1: uses the soft API, contains a hard API alternative"
		fi
	    else
		report "$1: does not contain an alternative API binary"
	    fi
	else
	    warn "Unable to read file: $1"
	fi
    shift
  done
}

# Parses a GCC command line.  The script has already determined
# that there are no script specific swithes on the command line.
parse_args ()
{
    local optname optarg

    while [ $# -gt 0 ]
    do
	optname="`echo $1 | sed 's,=.*,,'`"
	optarg="`echo $1 | sed 's,^[^=]*=,,'`"
	case "$1" in
	    # Look for options controlling verbosity.
	    # FIXME: we only do a simplistic scan.  Ideally we would
	    # scan the command line twice, once for verbosity switches
	    # and then afterwards for other types of switches.
	    --verbose | -v)
		if [ $SHOW_PROGRESS -eq -1 ]
		then
		    SHOW_PROGRESS=-2
		else
		    SHOW_PROGRESS=2
		fi
		;;
	    --quiet | -q)
		if [ $SHOW_PROGRESS -gt 0 ]
		then
		    SHOW_PROGRESS=0
		fi
		;;

            # Look for filenames
	    -o)
		if test "x$output_file" != "x" ;
		then
		    fail "Multiple output file names detected"
		else
		    output_file=$2
		    verbose "Output file name: $output_file"
		fi
		;;
	    *.c)
		if test "x$source_file" == "x" ;
		then
		    source_file=$optname
		    verbose "Source file: $source_file"
		else
		    warn "Multiple source files detected.  This is not currently supported."
		    exit 1
		fi
		;;

	    # FIXME: Add C++ and other source file types...
	    
	    # Look for compling versus linking.
	    -c)
		compilation=1
		;;

	    # We are not interested in any other command line options.
	    -*)
		;;

	    # Anything else is assumed to be a filename,.
	    # This fails with switches that take an argument.
	    *)
		object_files+=$optarg
		;;
	esac
	shift
    done
}

# Execute the rest of the command line, after the script specific
# switches have been removed.
process_files ()
{
    compilation=0
    source_file=""
    output_file=""

    # Create an array for object files
    declare -a object_files
    
    parse_args ${1+"$@"}

    if [ $compilation -eq 1 ]
    then
	perform_compile ${1+"$@"}
    else
	perform_link ${1+$@}
    fi
}

#-------------------------------------------------------

main ()
{
    # Initialise user modifiable variables.
    init_user

    # Initialise internal variables.
    init_internal

    if test "x$1" = "x" ;
    then
	warn "use --fat-help option to see a description of this script"
	exit 1
    fi

    while [ $# -gt 0 ]
    do
        # For simplicity we specify that any script
	# specific switch(es) must come first.
	case "$1" in
	    --fat-version)
		version
		;;
	    --fat-help)
		help
		;;
	    --fat-save-temps)
		save_temps=1
		;;
	    --fat-status)
		shift
		show_status ${1+"$@"}
		break
		;;
	    --fat-swap)
		shift
		swap_files ${1+"$@"}
		break
		;;
	    --fat-verbose)
		SHOW_PROGRESS=2
		shift
		;;
	    *)
		process_files ${1+"$@"}
		break
		;;
	esac
    done

    exit 0
}

#-------------------------------------------------------

# Invoke main
main ${1+"$@"}
