#!/bin/zsh

set -e
setopt null_glob

# This script needs zsh, git-buildpackage, sbuild installed and
# A chroot called "unstable-amd64-sbuild"

LOCALPATH=$(realpath $(dirname $0))
export LOCALPATH

source cross-gcc-dev-helpers.sh




# First I parse the cmdline options
typeset -A cmdline_options
gcc_release=""
subcommand=""
{
    usage="Usage: $0 [--norefresh] [--experimental] [--nogcc_latest_check] [--sbuild_opts ... ]
     gcc_release [subcommand]
     Known subcommands:
           update_patches
           import_patches
           builddev"
    zparseopts -D -A cmdline_options \
               h -help -norefresh -experimental -nogcc_latest_check \
               -sbuild_opts:

    gcc_release="$1"
    subcommand="$2"

    if (( cmdline_options[(I)-h] + cmdline_options[(I)--help] )); then
        echo $usage;
        exit 0;
    fi

    if [[ -z "${gcc_release}" ]]; then
        echo "Need gcc_release on the commandline";
        echo $usage;
        exit 1;
    fi

    validate_gcc_source_release "${gcc_release}" ||
        {
            echo "Usage error! Given version '${gcc_release}' not in the known-version list '${known_gcc_releases[@]}'";
            echo ""
            echo $usage
            exit 1
        }

    if [[ ! "$subcommand" =~ "^(update_patches|import_patches|builddev|)$" ]]; then
        echo "Subcommand '$subcommand' unrecognized"
        exit 1        
    fi
}
function have_opt
{
    (( cmdline_options[(I)$1] ))
}


set -u
set -x    






export DEB_BUILD_OPTIONS=parallel=4
export DH_VERBOSE=1

gcc_dir="${HOME}/debianstuff/gcc-${gcc_release}"
cross_gcc_repo="git@salsa.debian.org:crosstoolchain-team/cross-gcc.git"

cross_gcc_dir="${HOME}/debianstuff/cross-gcc"
gcc_repo="git@salsa.debian.org:crosstoolchain-team/gcc-${gcc_release}.git"

build_subdir="build"

# for git commits and debian/changelog entries written by dch
export DEBEMAIL=dima@secretsauce.net
export DEBFULLNAME="Dima Kogan"
export DEBSIGN_KEYID=dima@secretsauce.net




# I want the ability for the script to exit early in response to some errors.
# Due to the way shells work, some functions run in a sub-shell, and invoking
# "exit" does not exit the whole script even with set -e. So instead I kill
# myself with 'kill $$' in those cases. This trap serves to handle the kill, and
# print a nice message
TRAPTERM()
{
    print "Script gave up due to errors"
    return $(( 128 + 1 ))
}



function refresh_sources
{
    # I make sure the gcc and cross-gcc sources are checked-out locally, and are
    # up-to-date relative to their remote git repos

    typeset -A dirs_repos

    dirs_repos=($cross_gcc_dir $cross_gcc_repo $gcc_dir $gcc_repo)
    for dir (${(k)dirs_repos})
    {
        local repo=$dirs_repos[$dir]

        # make directories that don't exist
        if ! [[ -d $dir ]]; then
            mkdir -p $dir;
        fi

        # check out new sources, if needed
        if ! [[ -d $dir/.git ]]; then
            ( cd $dir
              git init
              git remote add origin $repo
            )
        fi

        # pull down the most recent code
        ( cd $dir
          git fetch origin

          # we get the latest sources. The active branch list varies a little
          # bit between the various repos
          typeset -a branches
          branches=(master)
          if [ $repo = $gcc_repo ]; then
              branches+=(upstream patches)
          fi
          for branch ($branches)
          {
              if git show-ref --head origin/$branch | awk '{print $1}' | uniq -d | grep -q .; then
                  echo "origin/$branch is at the same ref as HEAD, so don't need to rebase"
              else

                  git checkout $branch -- || git checkout -b $branch origin/$branch --
                  git rebase origin/$branch
              fi
          }
        )
    }
}

function get_cross_gcc_dev_latest_pkgfile
{
    echo ${cross_gcc_dir}/${build_subdir}/cross-gcc-dev_*_all.deb(om[1])
}

function get_cross_gcc_dev_latest_pkgfile_version
{
    local filename=$(get_cross_gcc_dev_latest_pkgfile)
    [ -z ${filename:-} ] && return 0;

    filename=${filename:t}
    [ -z $filename ] && return 0;

    echo $filename | awk -F_ '{print $2}'
    true
}

function get_triplet
{
    dpkg-architecture -a${arch} -qDEB_HOST_GNU_TYPE -f 2>/dev/null
}

function latest_available_version
{
    rmadison -s unstable,experimental -u debian $* | tac | awk -F' *\\| *' '$2 {print $2; exit}'
}

function gcc_debian_release_for_SHA
{
    local sha=$1

    pushd $gcc_dir

    # I get the version from the debian/changelog. Previously I was using the
    # tag, but the tags can't have ~ in them, so the tag sometimes doesn't match
    # the debian version
    local version=$(git show $sha:debian/changelog | dpkg-parsechangelog -l- -S Version)

    if [ -z $version ]; then
        echo "Error: Couldn't get debian release of sha $sha"
        kill $$
    fi

    echo $version
    popd
}

function patches_are_up_to_date
{
    local version=$1

    patch1=($cross_gcc_dir/patches/gcc-${gcc_release}/0001*)
    if (( ! $#patch1 )); then
        echo "First patch doesn't exist";
        return 1;
    fi

    local first_patch=$(awk '{print $2; exit}' $patch1[1])
    if [ -z $first_patch ]; then
        echo "Error: couldn't get SHA of first patch"
        kill $$
    fi

    local patches_at_release=$(gcc_debian_release_for_SHA $first_patch)


    if dpkg --compare-versions $version '>>' $patches_at_release; then
        echo "Patches are out of date; need updating"
        return 1 # patches out of date
    elif dpkg --compare-versions $version '<<' $patches_at_release; then
        echo "Patches live in the future... Giving up"
        kill $$
    else
        echo "Patches are up-to-date"
        return 0
    fi

    true
}

function update_patches_in_git
{

    local version=$1

    # if we're up to date, return 1 to suppress patch updates
    patches_are_up_to_date $version && return 1

    # ok. patches are out of date, so we need to update them
    pushd

    # import new sources, if needed
    local version_in_tree=$(gcc_debian_release_for_SHA master)
    cd $gcc_dir
    if dpkg --compare-versions $version '>>' $version_in_tree; then

        echo "gcc tree needs to import the latest sources"

        sudo apt-get update
        gbp import-dsc --download "gcc-${gcc_release}-source=$version"

        # make sure the sources are actually new (APT server maybe isn't in
        # sync)
        local version_imported=$(gcc_debian_release_for_SHA master)
        if [ $version_imported != $version ]; then
            echo "Imported source version '$version_imported' does not match build version '$version'. Giving up"
            kill $$
        fi

    elif dpkg --compare-versions $version '<<' $version_in_tree; then
        echo "gcc tree lives in the future... Giving up"
        kill $$
    else
        echo "git tree is up-to date. Not importing anything."
    fi


    # I need to update the git tree. I
    # - update the patches to match the new source tree
    # - update debian/changelog
    # - update cross-gcc-dev-helpers.sh to require the most recent gcc sources.
    #   This is required because even though rmadison may see a package, it
    #   may not have made it to the repository yet. It makes sure I'm building
    #   what I think I'm building
    git checkout patches
    if ! git rebase master; then # this may fail if the patches do not apply cleanly
        echo "patch rebase failed. fix it and try again"
        kill $$
    fi

    pushd $cross_gcc_dir
    # +1 because these patches are about to go into the NEXT cross-gcc-dev
    # release
    local cross_gcc_dev_version_cur=$(( $(dpkg-parsechangelog --show-field=Version) + 1 ))
    popd
    git branch patches-$version-$cross_gcc_dev_version_cur

    popd
    true
}

function import_patches_from_git
{
    local version=$1

    pushd $cross_gcc_dir


    # don't try to delete patches that do not exist
    patches=(patches/gcc-${gcc_release}/*)
    if (( $#patches )); then
        git rm $patches[@] || true
    fi
    mkdir -p patches/gcc-${gcc_release}

    cd $gcc_dir
    git format-patch -o "$cross_gcc_dir/patches/gcc-${gcc_release}" master

    cd "$cross_gcc_dir/patches/gcc-${gcc_release}"
    ls *.patch > series

    popd
    true
}

function commit_patch_series
{
    local version=$1

    pushd "$cross_gcc_dir/patches/gcc-${gcc_release}"
    git add *
    git commit --author "$DEBFULLNAME <$DEBEMAIL>" -m 'patch update'

    popd
    true
}

function commit_finalizecross_gcc_dev_release
{
    local version=$1

    pushd $cross_gcc_dir
    export version
    perl -p -i -e "s{(known_debian_gcc_releases\\[${gcc_release}\\])=\".*?\"}
                    {\\1=\"\$ENV{version}\"}" cross-gcc-dev-helpers.sh
    git add cross-gcc-dev-helpers.sh

    # allow this to fail, which will happen if no changes are made
    git commit --author "$DEBFULLNAME <$DEBEMAIL>" -m "forcing build of gcc-${gcc_release}-source=$version" cross-gcc-dev-helpers.sh || true

    dch --no-auto-nmu "rebuild for $version"

    local release='unstable'
    have_opt --experimental && release='experimental'

    dch --no-auto-nmu -r $release
    git commit --author "$DEBFULLNAME <$DEBEMAIL>" -m 'version bump' debian/changelog


    # git push --all
    # git push origin +patches:patches
    # push both gcc and cross trees


    popd
    true
}

function build_cross_gcc_dev
{
    local version=$1

    echo "===== building cross-gcc-dev"

    pushd

    # if the patches need to be updated...
    update_patches_in_git $version &&
        {
            # ... then update them, and consider any error fatal
            import_patches_from_git $version &&
                commit_patch_series $version &&
                commit_finalizecross_gcc_dev_release $version ||
                    {
                        echo "Patch update failed. Need human intervention. Giving up"
                        kill $$
                    }
        }

    popd


    # We may have updated the cross-gcc-dev tree by updating the patches and the
    # changelog. If a new .deb needs to be built, we do it
    local cross_gcc_dev_version_cur=$(dpkg-parsechangelog --show-field=Version)
    local cross_gcc_dev_version_pkg=$(get_cross_gcc_dev_latest_pkgfile_version)

    if [[ -n "$cross_gcc_dev_version_cur" && $cross_gcc_dev_version_pkg = $cross_gcc_dev_version_cur ]]; then
        echo "cross-gcc-dev package up-to-date. Version $cross_gcc_dev_version_pkg";
        return 0;
    fi

    pushd $cross_gcc_dir
    rm -rf ${build_subdir}/cross-gcc-*(/)
    gbp buildpackage --changes-option=-S --git-ignore-new --git-export-dir=${build_subdir} -us -uc

    popd
}

function buildarch
{

    local arch=$1
    local version=$2

    build_failed=1 # I set this to 0 if successful

    echo "===== building arch $arch"

    # I check the libgcc1 build-dep. This isn't strictly necessary since sbuild
    # will fail to get the Build-Depends if libgcc1 isn't available. It is
    # however much quicker to check this here and now. sbuild takes a long time
    # to fail
    local libgcc_version=$(latest_available_version -a $arch libgcc1 | sed 's/.*://')
    if dpkg --compare-versions $version '>>' $libgcc_version; then
        echo "libgcc dependency not yet available. Giving up on build for now"
        return true
    fi


    pushd $cross_gcc_dir

    echo "Generating sources"

    TARGET_LIST="$arch" ./cross-gcc-gensource ${gcc_release}

    echo "Doing the sbuild"
    cd cross-gcc-packages-amd64/cross-gcc-${gcc_release}-$arch
    PARALLEL=${PARALLEL:=4}

    local sbuild_params
    sbuild_params=(--extra-package=$(get_cross_gcc_dev_latest_pkgfile)
                   -A -s
                   --build=amd64 --host=amd64
                   -c unstable-amd64-sbuild)

    if have_opt --experimental; then
        sbuild_params+=(-d experimental)
    else
        sbuild_params+=(-d unstable)
    fi

    have_opt --sbuild_opts && sbuild_params+=(${=cmdline_options[--sbuild_opts]})
    if sbuild ${sbuild_params[@]}; then
        build_failed=0

        # build successful!
    fi

    popd
}

function upload_base
{
    local base=$1

    # if we already uploaded, don't bother
    if [ ! -e "${base}.ftp-master.upload"]; then
        debsign ${base}.changes
        dput ftp-master ${base}.changes
    fi
}

function upload
{
    local arch=$1
    local version=$2

    # upload commands: Not yet daring to automate those
    return 0


    # I upload cross-gcc-dev and a toolchain for the given arch. If
    # cross-gcc-dev was already uploaded, I don't attempt it
    local latest_cross_gcc_dev_ver=$(get_cross_gcc_dev_latest_pkgfile_version)
    upload_base "${cross_gcc_dir}/${build_subdir}/cross-gcc_$(latest_cross_gcc_dev_ver)_all"
    upload_base "${cross_gcc_dir}/cross-gcc-packages-amd64/cross-gcc-${gcc_release}-${arch}_${version}_amd64"
}

function source_version
{
    if have_opt --nogcc_latest_check; then
        gcc_debian_release_for_SHA master
    else

        local result=$(latest_available_version gcc-${gcc_release}-source)

        if [ -z $result ]; then
            echo "Error: couldn't get latest source version"
            kill $$
        fi

        echo $result
    fi
}

function uploaded_version
{
    local arch=$1
    local arch_triplet=$(get_triplet $arch)

    # first I look in the .upload file written by dput and then in APT
    local result=$(< $cross_gcc_dir/cross-gcc-packages-amd64/cross-gcc-${gcc_release}-${arch}_*.upload(om[1]) awk '/\.deb\y/ { split( $3, a, "_"); print a[2]; exit}' 2> /dev/null; true)
    result=${result:=$(latest_available_version gcc-${gcc_release}-$arch_triplet)}

    echo ${result:=0}
}

function built_version
{
    local arch=$1
    local result=$(< $cross_gcc_dir/cross-gcc-packages-amd64/cross-gcc-${gcc_release}-${arch}_*.changes(om[1]) awk '/\.deb\y/ { split( $3, a, "_"); print a[2]; exit}' 2> /dev/null; true)
    result=${result/+/-}
    echo ${result:=0}
}

function try_all
{
    echo "Trying to build everything"

    new_ver=$(source_version)

    build_cross_gcc_dev $new_ver

    # build all out-of-date packages
    for arch ($arches[@]) {
            echo "======== Looking at arch $arch"

            if dpkg --compare-versions $new_ver '>>' $(built_version $arch); then
                buildarch $arch $new_ver

                if ((build_failed)); then
                    echo "WARNING: Failed to build arch $arch; skipping for now"
                fi
            else
                echo "Arch $arch is up-to-date; skipping build"
            fi

            if dpkg --compare-versions $new_ver '>>' $(uploaded_version $arch); then
                upload $arch $new_ver
            else
                echo "Arch $arch is already uploaded; skipping upload"
            fi

            echo "";
        }

        true
}


have_opt --norefresh || refresh_sources

# Can't do anything without a git copy of cross-gcc and gcc-4.9
if ! test -f ${cross_gcc_dir}/debian/changelog; then
    init_path ${cross_gcc_dir} 
    git clone ${cross_gcc_repo} ${cross_gcc_dir}
fi

if ! test -f ${gcc_dir}/debian/changelog; then
    init_path ${gcc_dir} 
    gbp-clone ${gcc_repo} ${gcc_dir}
fi

typeset -a arches
arches=($(cat $cross_gcc_dir/debian/targetlist))

if [ "$subcommand" = "update_patches" ]; then
    local version=$(source_version)

    update_patches_in_git $version &&
        import_patches_from_git $version &&
        commit_patch_series $version &&
        commit_finalizecross_gcc_dev_release $version
    exit;
fi

if [ "$subcommand" = "import_patches" ]; then
    local version=$(source_version)
    import_patches_from_git $version &&
        commit_patch_series $version &&
        commit_finalizecross_gcc_dev_release $version
    exit;
fi

if [ "$subcommand" = "builddev" ]; then
    echo "Only building cross-gcc-dev"

    local version=$(source_version)
    build_cross_gcc_dev $version
    exit;
fi



while (true) {
    try_all
    echo "===== Looked at all the arches. Sleeping"

    sleep 3600
}
