#! /bin/sh
#
# (C) Iain Sandoe May 2015.
#
# Try to mimic enough of the behaviour of the apple "driverdriver" and clang
# that we can build fat binaries using the normal GCC driver for most of the
# work.
#
# Install this in your toolchain 'bin' directory - it finds the toolchain
# pieces relative to its install.
#
# You would have (say):
#  .../bin/fat-gcc-N.M (iff your gcc is installed with a numerical suffix)
#  .../bin/fat-gcc (if not)
#
# Likewise:
#
#  .../fat-gfortran
#  .../fat-g++ / fat-c++
#
# TODO: figure out how to install this instead of the non-canonical target
#       compiler.
#
#
# Mostly this is about building multiple objects and lipo-ing them together,
# however it might also be necessary to partition arch-specific flags in some
# cases.
#
# TODO: Handle different source types, handle arch-specific options.
#
# version 0.1 - May 2015 - basic support for ppc and x86
# version 0.2 - July 2022 - add arm64.

prog="${0##*/}"
prog="${prog%.*}"
homedir="${0%/*}"
version="xtools - $prog 0.7.0"

destprog="${prog#*fat-}"
archs=0
ppc=0
ppc64=0
x86=0
x86_64=0
arm64=0
new_cl=
linking=1
non_link_opt_seen=0
seen_e_or_s=
seen_c=0
b_path=
output_name=
seen_version=0
seen_help=0
seen_v=0
seen_dylib=0
seen_hashes=0
min_vers=
save_temps=
seen_save_temps=0
other_arg_count=0
no_infiles=

while [ $# -ne 0 ]; do
   case $1 in
	--version) seen_version=1; new_cl="${new_cl} $1"; no_infiles="${no_infiles} $1"; shift ;;
	--help*) seen_help=1; new_cl="${new_cl} $1"; no_infiles="${no_infiles} $1"; shift ;;
	-###) seen_hashes=1; new_cl="${new_cl} $1"; no_infiles="${no_infiles} $1"; shift ;;
	-v) seen_v=1; new_cl="${new_cl} $1"; no_infiles="${no_infiles} $1"; shift;;
	-save-temps*) seen_save_temps=1; save_temps=$1; shift;;
	-arch)
	    ((archs+=1)); shift
	    case $1 in
	        ppc64) ppc64=1;;
	        ppc*) ppc=1; ppc_arch=$1;;
	        i386) x86=1; x86_arch=$1;;
	        i686) x86=1; x86_arch=$1;;
	        x86_64) x86_64=1;;
	        arm64 | aarch64) arm64=1;;
	        *) echo "$prog : Unknown arch: $1 (ignored)";;
	    esac
	    shift
	    ;;
	-mmacosx-version-min=*)
            min_vers="${1#*=}";
            no_infiles="${no_infiles} $1";
	    new_cl="${new_cl} $1";
            shift
            ;;
        -S|-E)
            seen_e_or_s=$1;
            linking=0
            no_infiles="${no_infiles} $1";
	    new_cl="${new_cl} $1";
	    shift
	    ;;
        -c)
            seen_c=1;
            linking=0
            no_infiles="${no_infiles} $1";
	    new_cl="${new_cl} $1";
	    shift
	    ;;
	-B)
	    # We care about picking up auxiliary commands (like lipo) from
	    # overridden PATHs.
	    new_cl="${new_cl} $1 $2";
	    no_infiles="${no_infiles} $1 $2";
	    b_path="$b_path$2:"; shift 2
	    ;;
	-B*)
	    new_cl="${new_cl} $1";
            no_infiles="${no_infiles} $1";
	    b_path="$b_path${1#*B}:"; shift
	    ;;
	-o)
	    output_name=$2;
	    shift 2
	    ;;
	-dynamiclib | -shared)
	    seen_dylib=1
            no_infiles="${no_infiles} $1";
	    new_cl="${new_cl} $1";
	    shift
	    ;;
	-I | -L | -framework | -compatibility_version | -current_version | -install_name | -x)
            no_infiles="${no_infiles} $1 $2";
	    new_cl="${new_cl} $1 $2";
	    shift 2
	    ;;
	-*)
	    new_cl="${new_cl} $1";
            no_infiles="${no_infiles} $1";
	    shift
	    ;;
	*)
	    # keep the remainder of the positional args.
	    other_args[other_arg_count]="$1"
	    (( other_arg_count += 1))
	    new_cl="${new_cl} $1";
	    shift
	    ;;
    esac
done

if [ $archs -le 1 ]; then
  if [ $ppc -eq 1 ]; then new_cl="${new_cl} -arch $ppc_arch"; fi
  if [ $x86 -eq 1 ]; then new_cl="${new_cl} -arch $x86_arch"; fi
  if [ $ppc64 -eq 1 ]; then new_cl="${new_cl} -arch ppc64"; fi
  if [ $x86_64 -eq 1 ]; then new_cl="${new_cl} -arch x86_64"; fi
  if [ $arm64 -eq 1 ]; then new_cl="${new_cl} -arch arm64"; fi
  if [ -n "$output_name" ]; then new_cl="${new_cl} -o $output_name"; fi
  #echo "$prog : $destprog $new_cl $save_temps"
  exec $destprog $new_cl
fi

# OK - so it's not a simple case, make sure we detect when we're cross-compiling
# from a non-darwin host.

is_darwin=0
is_ppc=0
is_arm64=0
default_darwin_vers=9
default_darwin_arch="i686"
host_info=`uname -srm`
read sys vers narch <<< "$host_info"
case $sys in
    Darwin)
        is_darwin=1;
        case $narch in
           # We don't get anything to indicate a powerpc64 platform (because there
           # never was - a public - one).
           Power*) default_darwin_arch="powerpc"; is_ppc=0;;
           x86_64) default_darwin_arch="x86_64"; is_x86=1 ;;
           i386) default_darwin_arch="i686"; is_x86=1;;
           Arm64) default_darwin_arch="arm64"; is_arm64=1;;
           *) echo "$prog : didn't recognise the arch from $narch";;
        esac
        # Drop any micro version
        default_darwin_vers="${vers%.*}"
        # Drop any mini version
        default_darwin_vers="${default_darwin_vers%.*}"
	;;
    *) ;;
esac

if [ -n "$min_vers" ]; then
   maj="${min_vers#darwin.}"
   maj="${maj%.*}"
   if [ $maj -le 10 ]; then
     # Lose the pointless 10.
     darwin="${min_vers#*10.}"
     # Lose the minor,
     darwin="${darwin%.*}"
     (( darwin += 4 ))
   else
     (( darwin += 9 ))
   fi
elif [ $is_arm64 -eq 1 ]; then
    darwin=20
else
    darwin=$default_darwin_vers
fi


#echo "sys $sys version $vers native arch $narch (darwin vers $darwin)"

case $seen_e_or_s in
    -S)
        echo "$prog : error: cannot use 'assembler' output with multiple -arch options"
        exit 1
        ;;
    -E)
        echo "$prog : error: cannot use 'c++-cpp-output' output with multiple -arch options"
        exit 1
        ;;
    *) ;;
esac

# So now we want to build a per arch preferred toolname.
# This is a bit icky, we're spawning commands to check if we can find compilers,
# I suppose we could just assume that they're installed and fail at runtime.

arch_idx=0
if [ $ppc -eq 1 ] || [ $ppc64 -eq 1 ]; then
    if [ $ppc -eq 1 ]; then
        arch_list[$arch_idx]="$ppc_arch";
        arch_type[$arch_idx]=0;
        (( arch_idx += 1 ))
    fi
    if [ $ppc64 -eq 1 ]; then
        arch_list[$arch_idx]="$ppc64";
        arch_type[$arch_idx]=0;
        (( arch_idx += 1 ))
    fi
    ppc_cmd="$homedir/powerpc-apple-darwin$darwin-$destprog"
    if ! which $ppc_cmd >/dev/null ; then
        ppc_cmd="$homedir/powerpc64-apple-darwin$darwin-$destprog"
        if ! which $ppc_cmd >/dev/null ; then
            echo "$prog : didn't find powerpc{,64}-apple-darwin$darwin-$destprog"
            if [ $is_darwin -eq 1 ] && [ ! $is_x86 ]; then
                echo "$prog : trying $homedir/$destprog"
                ppc_cmd="$homedir/$destprog"
            else
                # Fall back to most likely.
                ppc_cmd="powerpc-apple-darwin9-$destprog"
                echo "$prog : trying $ppc_cmd"
            fi
        fi
    fi
fi

if [ $x86 -eq 1 ] || [ $x86_64 -eq 1 ]; then
    if [ $x86 -eq 1 ]; then
        arch_list[$arch_idx]="$x86_arch";
        arch_type[$arch_idx]=1;
        (( arch_idx += 1 ))
    fi
    if [ $x86_64 -eq 1 ]; then
        arch_list[$arch_idx]="x86_64";
        arch_type[$arch_idx]=1;
        (( arch_idx += 1 ))
    fi
    x86_cmd="$homedir/x86_64-apple-darwin$darwin-$destprog"
    if ! which $x86_cmd >/dev/null ; then
        x86_cmd="$homedir/i686-apple-darwin$darwin-$destprog"
        if ! which $x86_cmd >/dev/null ; then
            echo "$prog : didn't find {x86_64,i686}-apple-darwin$darwin-$destprog"
            if [ $is_darwin -eq 1 ] && [ $is_x86 ]; then
                echo "$prog : trying $homedir/$destprog"
                x86_cmd="$homedir/$destprog"
            fi
        fi
    fi
fi

if [ $arm64 -eq 1 ]; then
    arch_list[$arch_idx]="arm64";
    arch_type[$arch_idx]=2;
    (( arch_idx += 1 ))
    arm64_cmd="$homedir/aarch64-apple-darwin$darwin-$destprog"
    if ! which $arm64_cmd >/dev/null ; then
        echo "$prog : didn't find aarch64-apple-darwin$darwin-$destprog"
    fi
fi

# The next easiest case is when we only need to worry about linking the result.
if [ -z $output_name ]; then
    output_name="a.out"
fi
idx=0
while [ $idx -lt $arch_idx ]; do
    if [ $seen_save_temps -eq 1 ]; then
        tdir="./$prog.$$.${arch_list[idx]}"
        save_temps="-save-temps=obj"
    else
        tdir="/tmp/$prog.$$.${arch_list[idx]}"
	save_temps=
    fi
    tdirs[idx]="$tdir"
    if [ $seen_hashes -eq 0 ]; then
        if ! mkdir -p $tdir ; then
            echo "failed to make $tdir"
        fi
    fi
    (( idx += 1 ))
done

if [ $linking -ne 0 ]; then
    extra_link_opts=
    if [ $seen_dylib -ne 0 ]; then
        extra_link_opts="-Wl,-arch_multiple,-final_output,$output_name"
    fi
    object_list=
    idx=0
    while [ $idx -lt $arch_idx ]; do
        object_arch="${tdirs[idx]}/$output_name"
        object_list="${object_list} $object_arch"
        cl=" -arch ${arch_list[idx]} $new_cl $extra_link_opts -o $object_arch $save_temps"
        if [ ${arch_type[idx]} -eq 0 ]; then
            #echo "$ppc_cmd $cl"
            if ! $ppc_cmd $cl ; then
                echo "$ppc_cmd $cl failed"
            fi
        elif [ ${arch_type[idx]} -eq 2 ]; then
            #echo "$ppc_cmd $cl"
            if ! $arm64_cmd $cl ; then
                echo "$arm64_cmd $cl failed"
            fi
        else
            #echo "$x86_cmd $cl"
            if ! $x86_cmd $cl ; then
                echo "$x86_cmd $cl failed"
            fi
        fi
        (( idx += 1 ))
    done
    if [ $seen_version -eq 1 ] || [ $seen_v -eq 1 ]; then
        echo "$version"
        if [ $seen_version -eq 1 ]; then exit 0; fi
    fi
    if [ $seen_hashes -eq 1 ]; then
        lipo_to_use=`which lipo`
        echo "\"$lipo_to_use\" -create -o $output_name $object_list"
        exit 0
    fi
    if ! lipo -create -o $output_name $object_list ; then
        echo "lipo -create -o $output_name $object_list ... failed"
        exit 1
    fi
    if [ $seen_hashes -eq 0 ] && [ $seen_save_temps -eq 0 ]; then
        idx=0
        while [ $idx -lt $arch_idx ]; do
            #echo "removing : ${tdirs[idx]}"
            rm -r "${tdirs[idx]}"
            (( idx += 1 ))
        done
    fi
    exit 0
fi

# Now the longest-winded of them all.
if [ $seen_c -ne 0 ]; then
    idx=0
    while [ $idx -lt $other_arg_count ]; do
        name="${other_args[idx]}"
        name="${name##*/}"
        oname="${name%.*}"
        oname="${oname}.o"
        #echo "source to build : ${other_args[idx]} => $name => $oname"
        aidx=0
        object_list=
        while [ $aidx -lt $arch_idx ]; do
            object_arch="${tdirs[aidx]}/$oname"
            object_list="${object_list} $object_arch"
            cl=" -arch ${arch_list[aidx]} $no_infiles $extra_link_opts -o $object_arch $save_temps ${other_args[idx]}"
            if [ ${arch_type[aidx]} -eq 0 ]; then
                 #echo "$ppc_cmd $cl"
                 if ! $ppc_cmd $cl ; then
                     echo "$ppc_cmd $cl failed"
                fi
            elif [ ${arch_type[aidx]} -eq 2 ]; then
                 #echo "$arm64_cmd $cl"
                 if ! $arm64_cmd $cl ; then
                     echo "$arm64_cmd $cl failed"
                fi
            else
                #echo "$x86_cmd $cl"
                if ! $x86_cmd $cl ; then
                    echo "$x86_cmd $cl failed"
                fi
            fi
           (( aidx += 1 ))
         done
         if [ $seen_hashes -eq 1 ]; then
            lipo_to_use=`which lipo`
            echo "\"$lipo_to_use\" -create -o $oname $object_list"
         elif ! lipo -create -o $oname $object_list ; then
            echo "lipo -create -o $oname $object_list ... failed"
         fi
#         if [ $seen_hashes -eq 0 ] && [ $seen_save_temps -eq 0 ]; then
#            rm -r "$object_list"
#         fi
        (( idx += 1))
    done
fi
if [ $seen_hashes -eq 0 ] && [ $seen_save_temps -eq 0 ]; then
    idx=0
    while [ $idx -lt $arch_idx ]; do
        #echo "removing : ${tdirs[idx]}"
        rm -r "${tdirs[idx]}"
        (( idx += 1 ))
    done
fi

#echo "$prog : #archs : $archs : ppc=$ppc_arch x86=$x86_arch ppc64=$ppc64 x86_64=$x86_64 arm64=$arm64"
#echo "$prog : Bpaths : $b_path : min_vers $min_vers linking $linking"
#echo "$prog : new commandline : $new_cl (arcs $arch_idx archlist : $arch_list)"

exit 0
