#!/bin/sh

# gcc_select [-n] [-force] [2 | 3 | 3.x | 4.x ] [-h | --help] [-v | --version]
#            [-l | --list] [-root]
#            
#
# Switch the default gcc to any of 2.x, 3.x, or 4.x.
#
#  2             Select gcc 2.95.2 as the default compiler.
#  3             Select gcc 3.1 as the default compiler.
#  3.x           Select gcc 3.x as the default compiler.
#  4.x           Select gcc 4.x as the default compiler.
#  -force        Ensure the links are correct for the specified version.
#                even if the one specified is the current version.
#  -h            Display this help info.
#  --help        Same as -h.
#  -n            Show commands to do selection but do not execute them.
#  -root         Skip 'root' check and assume you have root access.
#  -v            Display gcc_select version number.
#  --version     Same as -v.
#  -l		 Display available compiler versions.
#  --list	 Same as -l.
#           
# This script will switch the current compiler installation around
# among 2.x, 3.x, and 4.x (or echo the commands to do it if -n is used).
# The current version is displayed if no arguments are specified.
#
# Note, for documentation completeness, there are three additional options.
#
#  -dstroot dir  Install the sym links in a /usr directory within the
#		 specified dir or the dir itself if it specifies a usr
#                directory (e.g., /foo/bar/usr).  -dstroot /usr is
#                allowed, in which case, the sytem is changed to the
#                requested compiler, i.e., the same effect as not
#                specifying it at all. 
#  --wrap SCRIPT Forces all possible mechanisms for invoking the
#		 compiler to be diverted from /usr/bin/NAME to
#		 /usr/bin/NAME.real, and /usr/bin/NAME will become
#		 a symbolic link to SCRIPT.  Will fail if binaries
#		 are already diverted.
#  --unwrap	 Restores diversions created by --wrap.
#
# These are special options NOT for general use.  They are deliberately 
# omitted from the --help info.  They are intended for internal build
# procedures.
#
# Copyright Apple Computer, Inc. 2002, 2003, 2004, 2005

#######################################################################

GCC_SELECT_VERSION="2.14"

#######################################################################

# List of headers in & underneath /usr/include that need to be symlinked
# to a compiler-specific version.

symlink_hdrs0="stdint.h"

# List of libraries in /usr/lib and /usr/local/lib that need to be 
# symlinked to a compiler-specific version.

symlink_libs="libcc_dynamic.a	\
	libcc_kext.a		\
	libgcc.a		\
	libcc.a			\
	libstdc++.a		\
	libsupc++.a"
symlink_local_libs="libcc_noc++.a"

# List of directories (underneath /usr) containing a 'default' 
# symlink that must be adjusted to correspond to the default compiler.

default_dirs="include/gcc/darwin	\
	libexec/gcc/darwin/i386		\
	libexec/gcc/darwin/ppc		\
	lib/gcc/darwin"


#######################################################################
#
## gcc_select's main control function
#
switch_it()
{
    local f who status argc=$# cwd="`/bin/pwd`"
    local n="\n`echo "usage: ${0##*/}" | sed -e 's/./ /g'`"
    local usage="${0##*/} [-n] [-force] [2 | 3 | 3.x | 4.x ] [-h | --help] [-v | --version]$n [-l | --list] [-root]"
    
    #
    # Collect the arguments...
    #
    dashn=
    switchto_cc=0
    switchto_cc_driver=0
    forced=
    list_versions=
    usr="/usr"
    installing=
    show_help=
    root=
    wrapper=
    unwrap=0
    
    while [ $# -gt 0 ] ; do 
	case $1 in
	    2* | -2* | 3* | -3* | 4.* | -4.* )
		if [[ "$switchto_cc" != "0" && "$switchto_cc" != "$1" ]]; then
		    echo -e "usage: $usage"
		    echo    "       Inconsistent compiler versions specified."
		    exit 1
		fi
		switchto_cc=$1
		shift
		;;
	    -force | --force)
		forced=1
		shift
		;;
	    -l | -list | --list)
		list_versions=1
		shift
		;;
	    -h | --help | -help | -\?)
	    	show_help=1
	    	shift
		;;
	    -n)
		dashn="echo -e ""\040"
		shift
		;;
	    -dstroot | --dstroot)
		shift
		usr="`echo "$1" | sed -e 's,/$,,'`"
		if [ "${usr##*/}" != "usr" ]; then
		    usr="$usr/usr"
		fi
		installing=1
		shift
		;;
	    -wrap | --wrap)
	        if [ $unwrap -ne 0 ]; then
	            echo "Cannot specify both --wrap and --unwrap."
	            exit 1
	        fi
	        shift

	        if [ -z "$1" ]; then
		    cat <<WRAP_HELP
Invalid --wrap option, please specify wrapper script after --wrap.

Writing Wrapper Scripts
-----------------------
Wrapper scripts should pass \$0 (argv[0]) through to the underlying program,
since drivers may use $0 to determine behavior.  Here are a couple of
examples of how to write clean wrappers:
	sh no-op wrapper: exec -a "\$0" "\$0.real" "\$@"
 	perl no-op wrapper: exec {"\$0.real"} (\$0, @ARGV);
WRAP_HELP
	            exit 1
                fi

	        wrapper="$1"
	        shift
	        ;;
	    -unwrap | --unwrap)
	        if [ -n "$wrapper" ]; then
	            echo "Cannot specify both --wrap and --unwrap."
	            exit 1
	        fi

	        unwrap=1
	        shift
	        ;;
	    -root)
	    	root=1
	    	shift
	    	;;
	    -v | -version | --version)
	    	echo "gcc_select v$GCC_SELECT_VERSION"
	    	exit 0
	    	;;
	    *)
		echo -e "usage: $usage"
		echo    "       Invalid argument ($1)."
		exit 1
		;;
	esac
    done
    
    if [ "$show_help" ]; then
	echo
	echo -e "usage: $usage"
	echo
	echo "2          Select gcc 2.95.2 as the default compiler."
	echo "3          Select gcc 3.1 as the default compiler."
	echo "3.x        Select gcc 3.x as the default compiler."
	echo "4.x        Select gcc 4.x as the default compiler."
	echo "-force     Ensure the links are correct for the specified version"
	echo "           even if it maches the current default version."
	echo "-h         Display this help info."
	echo "--help     Same as -h."
	echo "-l         List available compiler versions."
	echo "--list     Same as -l."	
	echo "-n         Show commands to do selection but do not execute them."
	echo "-root      Skip 'root' check and assume you have root access."
	echo "-v         Display gcc_select version number."
	echo "--version  Same as -v."
	echo
	exit 0
    fi

    if [ "$list_versions" ]; then
      echo "Available compiler versions:"
      cd /usr/include/gcc/darwin
      command ls -d [234]*
      exit 1
    fi
    #
    # We determine which compiler is currently installed by looking at
    # the version number it displays when we do a cc -v...
    #
    actual_ver="`cc -v 2>&1  | grep -i 'gcc version'`"
    #current_cc="`echo \"$actual_ver\" | sed -e 's/.*gcc version \([^ ]*\).*/\1/'`"
    current_cc="`echo \"$actual_ver\" | sed -e 's/.*gcc version \([0-9]*\.[0-9]*\).*/\1/'`"
    if [[ ! "$current_cc" == 2* && ! "$current_cc" == 3* && ! "$current_cc" == 4* ]]; then
        if [ $unwrap -eq 1 ]; then
            if [ -z "$forced" -o -z "$switchto_cc" ]; then
                echo "To restore from an invalid GCC wrapper, use gcc_select --force --unwrap 3.3"
                exit 1
            else
                current_cc=$switchto_cc
            fi
        else
            echo "Error trying to determine current cc version (got $current_cc)"
            exit 1
        fi
    fi

    # Handle wrapping and unwrapping
    if [ -n "$wrapper" ]; then
        if [ ! -f "$wrapper" ]; then
            echo "GCC wrapper doesn't exist."
            exit 1
        fi

        for driver in $usr/bin/{gcc,g++,c++,cpp,gcov}-* ; do
            case "$driver" in
                *.real)
                    # Ignore
                    ;;
                *)
                    if [ -L "$driver" -a -f "$driver.real" ]; then
                        echo "Sorry, $driver is already wrapped and we're trying to wrap it again."
                        if [ -z "$forced"  ]; then exit 1 ; fi
                    fi

                    if [ -f "$driver.real" ]; then
                        echo "Error, $driver already appears to be wrapped but there is no link to it."
                        if [ -z "$forced" ]; then exit 1 ; fi
                    fi

                    if [ -L "$driver" ]; then
                        $dashn ln -sf "`readlink $driver`.real" "$driver.real" || {
                            echo "Couldn't symlink $driver.real to `readlink $driver`.real"
                            if [ -z "$forced" ]; then exit 1 ; fi
                        }
                    else
                        $dashn mv "$driver" "$driver.real" || {
                            echo "Couldn't move $driver to $driver.real"
                            if [ -z "$forced" ]; then exit 1 ; fi
                        }
                    fi

                    $dashn ln -sf "$wrapper" "$driver" || {
                        echo "Couldn't make symbolic link from $driver to $wrapper"
                        $dashn mv "$driver.real" "$driver" || echo "...and couldn't restore $driver.real to $driver"
                        if [ -z "$forced" ]; then exit 1 ; fi
                    }
                    ;;
            esac
        done

        # Re-select current version so that symlinks go to .real
        forced=1
    elif [ $unwrap -ne 0 ]; then
        for driver in $usr/bin/{gcc,g++,c++,cpp,gcov}-*; do
           case "$driver" in
                *.real)
                    if [ -L "$driver" ]; then
                        $dashn rm -f "$driver" || {
                            echo "Couldn't remove $driver"
                            if [ -z "$forced" ]; then exit 1 ; fi
                        }
                    fi
                    ;;
                *)
                    DRIVER_LINK=
                    if [ -f "$driver.real" -o -L "$driver.real" ]; then DRIVER_LINK=1 ; fi

                    if [ -L "$driver" -a -n "$DRIVER_LINK" ]; then
                        $dashn rm -f "$driver" || {
                            echo "Couldn't remove symbolic link $driver"
                            if [ -z "$forced" ]; then exit 1 ; fi
                        }

                        if [ -f "$driver.real" ]; then
                            $dashn mv "$driver.real" "$driver" || {
                                echo "Couldn't move $driver.real to $driver"
                                if [ -z "$forced" ]; then exit 1 ; fi
                            }
                        fi
                    else
                       echo "Sorry, $driver does not appear to be wrapped."
                       if [ -z "$forced" ] ; then exit 1 ; fi
                    fi
                    ;;
            esac
        done

        # Re-select current version so that symlinks *don't* go to .real
        forced=1
    fi

    # No arguments means display only the current compiler version...
    #   
    if [ $argc -eq 0 ] || [ $argc -eq 1 -a "$show_ver" ]; then
	echo "Current default compiler:"
        echo "$actual_ver"
        exit 0
    fi  
    # if -force is specified without a compiler version, simply refresh the
    # currently installed compiler
    if [ "$forced" ] && [ "$switchto_cc" == "0" ]; then
      switchto_cc=$current_cc
    fi    

    # From here on we must know what we're switching to...
    # Handle 2.95.2 and 3.1 compiler driver names specially, 
    # for compatibility with Jaguar systems.  Other version numbers 
    # should simply have a dash prepended to them, yielding driver
    # names such as "gcc-3.3", "g++-3.4", etc.
    #
    case "$switchto_cc" in
      0)
    	echo -e "usage: $usage"
    	echo    "       You did not specify a compiler version."
    	exit 1
	;;
      2 | 2\.95\.2)
  	switchto_cc_driver="2"
	switchto_cc="2.95.2"
	;;
      3 | 3\.1)
	switchto_cc_driver="3"
	switchto_cc="3.1"
	;;
      *)
	switchto_cc_driver="-$switchto_cc"
	;;
    esac  

    #
    # If actually modifying the system do additional checks...
    #
    if [ ! "$installing" -a "$switchto_cc" != "0" ]; then
	#
	# If what we want to switch to is the same as the current installation
	# we have nothing more to do (unless we're "forced" to do it)...
	#
	if [ ! "$forced" -a "$switchto_cc" == "$current_cc" ]; then
	    echo "You are already using gcc version $current_cc as the default compiler."
	    exit 0
	fi

	#
	# The gcc_select script operates by setting various symlinks to point to
  	# compiler-version-specific files.  The following checks that we do not
	# accidentally clobber non-symlink files while creating the symlinks. 
	#
        list=
        for driver in cc c++ gcc g++ cc.real c++.real gcc.real g++.real; do
	  if [ -e /usr/bin/$driver -a ! -L /usr/bin/$driver ]; then
	    list="$list /usr/bin/$driver"
	  fi
	done
        for header in $symlink_hdrs0 ; do
          if [ -e /usr/include/$header -a ! -L /usr/include/$header ]; then
            list="$list /usr/include/$header"
          fi
	done
        for lib in $symlink_libs; do
          if [ -e /usr/lib/$lib -a ! -L /usr/lib/$lib ]; then
            list="$list /usr/lib/$lib"
          fi
	done
        for lib in $symlink_local_libs; do
          if [ -e /usr/local/lib/$lib -a ! -L /usr/local/lib/$lib ]; then
            list="$list /usr/local/lib/$lib"
          fi
	done
	if [ "$list" != "" ]; then
	    show_list "The following file(s) exist on your system but are NOT symlinks." \
			"Delete them before using gcc_select." $list
	    exit 1
	fi
	
	#
	# We must be running as "root" to be able to do the switching,
	# unless a `-root' has been specified.
	#
	if [ "$dashn" = "" -a ! "$root" ]; then
	    who=$UID
	    if [ $who != 0 ]; then
		echo '*******************************************'
		echo '*** THE gcc_select SCRIPT MUST BE RUN   ***'
		echo '*** AS root (OR WITH THE -root OPTION). ***'
		echo '*** NO CHANGES WERE MADE TO YOUR SETUP. ***'
		echo '*******************************************'
		exit 1
	    fi
	fi
    fi # usr
    
    #
    # Make sure that the compiler we are switching to has all the indispensable
    # drivers, headers and libraries.  Here we check only files common to all
    # compiler releases.
    #
    missing=
    #echo "switchto_cc=$switchto_cc   switchto_cc_driver=$switchto_cc_driver"
    for file_or_dir in \
	/usr/bin/gcc$switchto_cc_driver 		\
	/usr/bin/g++$switchto_cc_driver 		\
        /usr/include/gcc/darwin/$switchto_cc/stdint.h	\
	/usr/include/gcc/darwin/$switchto_cc/ ; do
      if [ ! -e $file_or_dir ]; then
	missing="$missing $file_or_dir"
      fi
    done

    # The following headers are compiler-provided on systems prior to Tiger,
    # and on Tiger systems with compilers prior to gcc 4.0.
    if [ $os_version -lt 100400 -o $switchto_cc \< 4.0 ]; then
      for file_or_dir in \
          /usr/include/gcc/darwin/$switchto_cc/float.h    \
          /usr/include/gcc/darwin/$switchto_cc/stdarg.h   \
          /usr/include/gcc/darwin/$switchto_cc/stdbool.h  \
          /usr/include/gcc/darwin/$switchto_cc/varargs.h  \
          /usr/include/gcc/darwin/$switchto_cc/ ; do
        if [ ! -e $file_or_dir ]; then
          missing="$missing $file_or_dir"
        fi
      done
    fi

    if [ "$missing" != "" ]; then
        show_list "The following is missing from your gcc $switchto_cc compiler installation." \
                  "Reinstall the $switchto_cc compiler, or use another release." $missing
        exit 1
    fi

    if [ $switchto_cc \< 3.5 ]; then
      if [ ! -e /usr/libexec/gcc/darwin/ppc/$switchto_cc ]; then
        "Files in /usr/libexec/gcc/darwin/ppc/$switchto_cc are missing from your gcc
        "compiler installation.  Reinstall the $switchto_cc compiler, or use another release." 
        exit 1
      fi
      if [ ! -e /usr/lib/gcc/darwin/ppc/$switchto_cc ]; then
        "Files in /usr/lib/gcc/darwin/ppc/$switchto_cc are missing from your gcc
        "compiler installation.  Reinstall the $switchto_cc compiler, or use another release." 
        exit 1
      fi
      # 3.5 and above
      # We ought to have an else clause here that checks that 
      # /usr/libexec/gcc/ppc-apple-darwin/4.0.0 exists here.
    fi 

    if [ "$dashn" != "" ]; then
      echo "Commands that would be executed if \"-n\" were not specified:"
    fi
    #
    # Set the sym links to point to the specified compiler tools.
    #
    $dashn mkdir -p $usr/bin
    $dashn rm -f $usr/bin/cc $usr/bin/c++

    # Handle wrappers
    if [ -L $usr/bin/gcc$switchto_cc_driver -o -n "$wrapper" ]; then
        $dashn ln -sf gcc$switchto_cc_driver.real $usr/bin/cc.real
        $dashn ln -sf g++$switchto_cc_driver.real $usr/bin/c++.real
        for driver in gcc g++ gcov; do
            $dashn ln -sf $driver$switchto_cc_driver.real $usr/bin/$driver.real
        done
    else
        $dashn rm -f $usr/bin/cc.real
        $dashn rm -f $usr/bin/c++.real
        for driver in gcc g++ gcov; do
            $dashn rm -f $usr/bin/$driver.real
        done
    fi

    $dashn ln -sf gcc$switchto_cc_driver $usr/bin/cc
    $dashn ln -sf g++$switchto_cc_driver $usr/bin/c++
    for driver in gcc g++ gcov; do
      $dashn rm -f $usr/bin/$driver
      if [ -x /usr/bin/$driver$switchto_cc_driver ]; then
	$dashn ln -sf $driver$switchto_cc_driver $usr/bin/$driver
      fi
    done

    # Change the various "default" sym links for
    # directories.
    for dir in $default_dirs; do
      if [ -d /usr/$dir ]; then
	$dashn mkdir -p $usr/$dir
	$dashn rm -f $usr/$dir/default
	$dashn ln -sf $switchto_cc $usr/$dir/default
      fi
    done

    # Set up the library symlinks...
    $dashn mkdir -p $usr/lib
    for lib in $symlink_libs; do
      $dashn rm -f $usr/lib/$lib
      if [ -e /usr/lib/gcc/darwin/default/$lib ]; then
        $dashn ln -sf gcc/darwin/default/$lib $usr/lib/$lib
      fi
    done
    $dashn mkdir -p $usr/local/lib
    for lib in $symlink_local_libs; do            
      $dashn rm -f $usr/local/lib/$lib
      if [ -e /usr/lib/gcc/darwin/default/$lib ]; then            
        $dashn ln -sf ../gcc/darwin/default/$lib $usr/local/lib/$lib                                   
      fi
    done
    # The functionality in what used to be libcc_dynamic.a in 2.95.2 now
    # resides in libgcc.a.
    if [ ! -e $usr/lib/libcc_dynamic.a ]; then
      $dashn ln -sf gcc/darwin/default/libgcc.a $usr/lib/libcc_dynamic.a
    fi

    # For gcc 3.5 and above, some of the symlinks we just set up point to nowhere.
    # Get rid of them.
    if [ $switchto_cc \> 3.3 ]; then
      $dashn rm -f $usr/lib/gcc/darwin/default
      $dashn rm -f $usr/lib/libcc_dynamic.a
    fi

    # Set up header symlinks...
    $dashn mkdir -p $usr/include
    for header in $symlink_hdrs0; do
      $dashn rm -f $usr/include/$header
      if [ -e /usr/include/gcc/darwin/default/$header ]; then
        $dashn ln -sf gcc/darwin/default/$header $usr/include/$header
      fi
    done
    # Set up man page symlinks...
    # NB: The crufty /usr/bin/cpp script utilizes the
    # /usr/libexec/gcc/darwin/{i386,ppc}/default/cpp binary to do its work.
    # Hence, it makes sense to symlink the cpp man page to the current compiler
    # default.
    $dashn mkdir -p $usr/share/man/man1
    for manpage in c++ g++ gcc gcov cpp; do
      $dashn rm -f $usr/share/man/man1/$manpage.1
      if [ -e /usr/share/man/man1/$manpage$switchto_cc_driver.1 ]; then
        $dashn ln -sf $manpage$switchto_cc_driver.1 $usr/share/man/man1/$manpage.1
      fi
    done
    $dashn rm -rf $usr/share/man/man1/cc.1    
    if [ -e /usr/share/man/man1/gcc.1 ]; then
	$dashn ln -sf gcc.1 $usr/share/man/man1/cc.1
    fi
    
    #
    # We determine which compiler is currently installed by looking at
    # the version number it displays when we do a cc -v...
    #
    if [ ! "$installing" ]; then
      if [ "$dashn" == "" ]; then
        actual_ver="`cc -v 2>&1  | grep -i 'gcc version'`"
        current_cc="`echo \"$actual_ver\" | sed -e 's/.*gcc version \([^ ]*\).*/\1/'`"
        if [[ ! "$current_cc" == 2* && ! "$current_cc" == 3* && ! "$current_cc" == 4* ]]; then
      	    echo "Error trying to determine current cc version (got $current_cc)"
     	    exit 1
        fi
        echo "Default compiler has been set to:"
        echo "$actual_ver"
      fi
    else
      echo "$(basename $0): SYMLINKS UNDER '$usr'"
      echo "$(basename $0): NOW POINT AT gcc$switchto_cc_driver ($switchto_cc)"
    fi
    exit 0
}

#---------------------------------------------------------------------#
#
## show_list info1 info2 item1 ... - display a list of items
##
## This outputs an error message about a list of "bad" items.  The
## format is:
##
## info1
## info2
##   item1
##   - - -
##
## The info2 line is not output if it is null.
#
show_list()
{
    local info1="$1"
    local info2="$2"
    
    shift 2
    
    if [ ${#@} -gt 0 ]; then
	echo "$info1"
	
	if [ "$info2" != "" ]; then
	    echo "$info2"
	fi
	
	for f; do
	    echo "  $f"
	done
    fi
}

#######################################################################
### main() ############################################################
#######################################################################

os_version=$(sw_vers -productVersion | awk -F. '{ printf "%02d%02d%02d", $1, $2, $3 }')
if [ $os_version -lt 100200 ]; then
  echo "You must be using MacOS X 10.2 (Jaguar) or later."
  exit 1
fi
switch_it "$@"
