#!/bin/sh

## This is the build script which creates the pre-compiled directory.  It is
## currently running from Eli's account on winooski, but it should be easy to
## configure to run anywhere.  It can update the repository, so instead of
## running it straight from there it is better to copy it someplace else before
## running so we get a clean copy.

# if we're not using it already, switch to bash
if [ "${BASH_VERSION:-not_bash}" = "not_bash" ]; then exec bash "$0" "$@"; fi

###############################################################################
### Configuration

# verbose output?
verbose="yes"
# should we record an external log at $buildlogfile? ("only" means only there)
buildlog="yes"
# should we make binaries?
make_bins="ask_or_yes"
# should we do a repository update?
make_repo="ask_or_yes"
# should we make the pdf docs directory?
make_pdf_docs="ask_or_yes"
# should we re-make the build directories?
make_builddirs="ask_or_yes"
# should we make the pre-install bundles?
make_bundles="ask_or_yes"
# should we make platform-specific installers?
make_installers="ask_or_yes"
# should we build all web pages (and distribute unless it's a release)
make_web="ask_or_yes"
# should we make prebuilt stuff available on the web page?
# (for major distributions, it will be in html/NNN instead of html/)
make_pre_web="ask_or_yes"
# should we run all test options? (multiple configurations)
run_all_tests="no"

# people to notify when a build starts
buildnotifyemail=""

# repository branches to use -- `master' or other branches
init_repo_vars() {
  # use this function to initialize these on remote builds too
  gitbranch="${RKTBRANCH:-master}"
  gitibranch="${RKTIBRANCH:-master}"
}
init_repo_vars

# main machine that runs the whole build (the expected `$hostname' value)
workmachine="winooski"
# main directory on $workmachine (should be absolute)
maindir="/home/scheme"

# machines for specific installer creations
dmgmachine="weatherwax"
nsismachine="pitcairn"

# list of environment variables that should be carried over to ssh jobs
ssh_vars=(RKTBRANCH RKTIBRANCH)

# Add stuff to be msetted later (when we have the `mset' function)
declare -a initial_msets machines
msets() {
  local m; for m; do initial_msets[${#initial_msets[*]}]="$m"; done
}
# shorthand for mset to define a build target
defbuild() {
  machines[${#machines[*]}]="$1"
  msets "/machines/$1" "platform=$2"; shift 2; msets "$@"
}

# Remote builds configuration, a table of /machines/<machine-name>/<field>
# entries, with misc fields set.  Machines and platforms must be unique.  The
# "D" first entry is for default field values (missing default makes the field
# required).  Warning: an `eval "foo=\"bar\""' is used to assign values.
msets "/machines/D" "workdir=/var/tmp" "moveto=" "copytobak=" \
      "configure_args=" "LDFLAGS=" "ext_lib_paths=" "renice=" \
      "more_setup_args=" "test_gui="
# defbuild "ccs-solaris" "sparc-solaris" "moveto=/proj/racket" \
#          "ext_lib_paths=/arch/unix/packages/openssl-0.9.7e"
defbuild "pitcairn" "i386-win32" "test_gui=yes" \
         "workdir=F:" # no "/..." path (that can get interpreted as a flag)
defbuild "pitcairn2" "x86_64-win32" "test_gui=yes" \
         "workdir=C:/build" # same here (see also commit log!)
# The LDFLAGS is a workaround for a bug in Fink, see
# http://wiki.finkproject.org/index.php/Fink:Packaging:Preparing_for_10.5#OpenGL_Bug
# defbuild "kauai"       "ppc-darwin" "configure_args=--enable-xonx --disable-mac64" \
#   "LDFLAGS=-dylib_file /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib:/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib"
defbuild "weatherwax"  "ppc-osx-mac" \
         "configure_args=--enable-sdk=/Developer/SDKs/MacOSX10.4u.sdk"
defbuild "macintel"    "i386-osx-mac" \
         "configure_args=--enable-sdk=/Developer/SDKs/MacOSX10.4u.sdk --disable-mac64"
# defbuild "galaga"      "i386-linux-ubuntu-hardy"
defbuild "champlain"   "i386-linux-f12"
defbuild "ccs-linux"   "i386-linux-ubuntu-karmic" "moveto=/proj/racket"
# defbuild "punge"       "i386-linux-ubuntu-jaunty" "renice=20"
# defbuild "bjorn"       "i386-linux-gcc2"
# defbuild "chicago"     "i386-linux-debian"
# defbuild "brownbuild"  "i386-linux-debian" # really an AMD64 machine
# defbuild "inga"        "i386-freebsd"
# defbuild "chicago-unstable" "i386-linux-debian-unstable"
defbuild "osx64" "x86_64-osx-mac"
# These builds are courtesy of Mason Loring Bliss <mason at blisses dot org>
# defbuild "blisses1" "x86_64-linux-debian-lenny"
defbuild "blisses2" "x86_64-linux-debian-squeeze"
# Start the main build last
defbuild "$workmachine" "x86_64-linux-f14" "copytobak=$maindir" "test_gui=yes"
msets "/"

###############################################################################
### Initialize & Setup environment

# allow core dumps
ulimit -c unlimited
# make sure that everything is group-readable
umask 002
# needed for some hacks below
shopt -s extglob

# get this script's name and path
cd "$(dirname "$0")"
buildscript="$(pwd)/$(basename "$0")"
# get the current hostname (short version)
hostname="$(hostname)"
hostname="${hostname%%.*}"

# web directory for pre-prelease stuff on $workmachine (relative to $maindir)
prewebdir_default="html"
# directory for installation (relative to $maindir)
installdir="racket"
# directory in racket for build-related scripts (includes this script)
scriptdir="collects/meta/build"
# directory for internal stuff (relative to $maindir)
internaldir="iplt"
# directories for clean repository checkouts (relative to $maindir)
cleandir="checkout"
# directory for binaries (relative to $maindir)
bindir="binaries"
# directory for pre-installers (relative to $maindir)
preinstdir="pre-installers"
# directory for installers (relative to $maindir)
installersdir="installers"
# directory for pre-installers (relative to $maindir)
instdir="installers"
# directory for docs (relative to $maindir)
docdir="docs"
# directory for web content (relative to $maindir)
webdir="web"
# where inside webdir do we find the pre pages template files
pretemplatedir="stubs/pre"
# script for patching files with current version info
versionpatcher="$scriptdir/versionpatch"
# DrScheme test script
drtestscript="$scriptdir/test-drracket.rkt"
# bundling script
bundlescript="$scriptdir/bundle"
# web build script
webscript="collects/meta/web/build.rkt"
# status script
statusscript="$scriptdir/current-build-status.cgi"
# url mirrors file (relative to $maindir)
knownmirrors="known-mirror-urls"
# sitemap materials
sitemapdir="$scriptdir/sitemap"

# platform-installer stuff, all relative to the racket tree
nsisdir="$scriptdir/nsis"
unixinstallerdir="$scriptdir/unix-installer"
unixpathcheckscript="$unixinstallerdir/check-install-paths"
unixinstallerscript="$unixinstallerdir/installer-header"

# full clean tgz before building anything (relative to $maindir)
cleantgz="${installdir}-clean-tree.tgz"
# log file for this script (relative to $maindir)
buildlogfile="build-log.txt"
# name of html files to generate for web directories
index="index.html"
# timestamp and version file for automatic scripts (relative to $maindir)
stampfile="stamp"

# directory for temporary stuff (absolute path) -- on all machines
tmpdir="/tmp/racket-build"
mkdir -p "$tmpdir" # make sure that it exists asap
if [[ "x$1" != "x--dispatch" ]]; then
  chcon --type=httpd_sys_content_t "$tmpdir" # and readable on main machine
fi
# lockfile for this script (date marks when the build started)
lockfile="$tmpdir/build-lock"
# status file, and a file to save it when done
statusfile="$tmpdir/status"
statusfile_last="$tmpdir/last_status"
# name for running this script remotely
remotebuildscript="$tmpdir/build"
# full name for clean repository tgz file to transfer for distributed builds
repotgz="$tmpdir/$cleantgz"
# full name for full tgz file (with binaries etc)
fulltgz="$tmpdir/$installdir-full.tgz"
# log file name prefix for background jobs
bglogfile="$tmpdir/bg-log"
# absolute prefix name for the web-available current log symlinks
curlog_prefix="$maindir/$prewebdir_default/current"

last_part() {
  echo "$*" | sed 's/.*[ -]//'
}
last_part_capital() {
  local word="$(last_part "$@")"
  echo "$(echo "${word:0:1}" | tr "[:lower:]" "[:upper:]")${word:1}"
}

# simple name associations
name_of_platform() {
  case "$1" in
    ( "i386-linux"        ) echo "Linux/i386/GCC3" ;;
    ( "i386-linux-gcc2"   ) echo "Linux/i386/GCC2" ;;
    ( "i386-linux-fc2"    ) echo "Linux/i386/Fedora Core 2" ;;
    ( "i386-linux-fc5"    ) echo "Linux/i386/Fedora Core 5" ;;
    ( "i386-linux-fc6"    ) echo "Linux/i386/Fedora Core 6" ;;
    ( "i386-linux-f7"     ) echo "Linux/i386/Fedora 7" ;;
    ( "x86_64-linux-f7"   ) echo "Linux/x86_64/Fedora 7" ;;
    ( "i386-linux-f9"     ) echo "Linux/i386/Fedora 9" ;;
    ( "i386-linux-f12"    ) echo "Linux/i386/Fedora 12" ;;
    ( "x86_64-linux-f14"  ) echo "Linux/x86_64/Fedora 14" ;;
    ( "i386-linux-debian"          ) echo "Linux/i386/Debian Stable" ;;
    ( "x86_64-linux-debian-"* ) echo "Linux/x86_64/Debian" \
                                     "$(last_part_capital "$1")" ;;
    ( "i386-linux-ubuntu"          ) echo "Linux/i386/Ubuntu" ;;
    ( "i386-linux-ubuntu-"* ) echo "Linux/i386/Ubuntu" \
                                   "$(last_part_capital "$1")" ;;
    ( "i386-freebsd"      ) echo "FreeBSD" ;;
    ( "sparc-solaris"     ) echo "Solaris" ;;
    ( "i386-osx-mac"      ) echo "Mac OS X (Intel 32-bit)" ;;
    ( "ppc-osx-mac"       ) echo "Mac OS X (PPC)" ;;
    ( "x86_64-osx-mac"    ) echo "Mac OS X (Intel 64-bit)" ;;
    ( "ppc-darwin"        ) echo "X11 on Darwin (PPC)" ;;
    ( "i386-darwin"       ) echo "X11 on Darwin (Intel)" ;;
    ( "i386-win32"        ) echo "Windows (x86, 32-bit)" ;;
    ( "x86_64-win32"      ) echo "Windows (x64, 64-bit)" ;;
    # These are source distribution platforms
    ( "unix"              ) echo "Unix" ;;
    ( "mac"               ) echo "Mac OS X" ;;
    ( "win"               ) echo "Windows" ;;
    ( * ) exit_error "Unknown platform name for name_of_platform \"$1\"" ;;
  esac
}
extra_description_of_platform() {
  local e=""
  case "$1" in
    ( *"-osx-mac" ) e="An OS X Build." ;;
    ( *"-darwin"  ) e="This is an X11 on Darwin build using";
                    e="$e<tt>--enable-xonx</tt>, not a standard OS X build." ;;
    ( "i386-linux" ) e="Binaries for GCC3 (eg, RedHat 9 and Fedora Core)." ;;
    ( "i386-linux-gcc2" ) e="Binaries for old GCC2 setups (eg, RedHat 7.x)." ;;
    ( "i386-linux-"*   ) e="A Linux i386 build" ;;
    ( "x86_64-linux-"* ) e="A Linux x86_64 build" ;;
  esac
  case "$1" in
    ( *"-linux-fc"[0-9]*        ) e="$e on Fedora Core ${1##*-fc}." ;;
    ( *"-linux-f"[0-9]*         ) e="$e on Fedora ${1##*-f}." ;;
    ( *"-linux-debian"          ) e="$e on Debian Stable." ;;
    ( *"-linux-debian-"* ) e="$e on Debian ($(last_part_capital "$1"))." ;;
    ( *"-linux-ubuntu"   ) e="$e on Ubuntu." ;;
    ( *"-linux-ubuntu-"* ) e="$e on Ubuntu ($(last_part_capital "$1"))." ;;
  esac
  if [[ "$e" != "" ]]; then echo "<br><small>${e}</small>"; fi
}
name_of_dist_package() {
  case "$1" in
    ( "mz"   ) echo "Racket Textual" ;;
    ( "plt"  ) echo "Racket" ;;
    ( "full" ) echo "Racket Full" ;;
    ( * ) exit_error "Unknown package name for name_of_dist_package: \"$1\"" ;;
  esac
}
name_of_dist_type() {
  case "$1" in
    ( "bin" ) echo "Binary" ;;
    ( "src" ) echo "Source" ;;
    ( * ) exit_error "Unknown type name for name_of_dist_type: \"$1\"" ;;
  esac
}
platforms_of_dist_type() {
  case "$1" in
    ( "bin" ) echo "i386-win32" \
                   "x86_64-win32" \
                   "i386-osx-mac" \
                   "ppc-osx-mac" \
                   "x86_64-osx-mac" \
                   "ppc-darwin" \
                   "i386-darwin" \
                   "i386-linux" \
                   "i386-linux-gcc2" \
                   "i386-linux-fc2" \
                   "i386-linux-fc5" \
                   "i386-linux-fc6" \
                   "i386-linux-f7" \
                   "x86_64-linux-f7" \
                   "i386-linux-f9" \
                   "i386-linux-f12" \
                   "x86_64-linux-f14" \
                   "i386-linux-debian" \
                   "i386-linux-debian-testing" \
                   "i386-linux-debian-unstable" \
                   "x86_64-linux-debian-lenny" \
                   "x86_64-linux-debian-squeeze" \
                   "i386-linux-ubuntu" \
                   "i386-linux-ubuntu-dapper" \
                   "i386-linux-ubuntu-edgy" \
                   "i386-linux-ubuntu-feisty" \
                   "i386-linux-ubuntu-hardy" \
                   "i386-linux-ubuntu-intrepid" \
                   "i386-linux-ubuntu-jaunty" \
                   "i386-linux-ubuntu-karmic" \
                   "i386-freebsd" \
                   "sparc-solaris" ;;
    ( "src" ) echo "win mac unix" ;;
    ( * ) exit_error "Unknown type name for platforms_of_dist_type: \"$1\"" ;;
  esac
}
installer_of_dist_type_platform() { # input: dtype-dplatform
  case "$1" in
    ( "src-unix"        ) echo "tgz"  ;;
    ( "src-mac"         ) echo "dmg"  ;;
    ( "src-win"         ) echo "zip"  ;;
    ( "bin-"*"-linux"*  ) echo "sh"   ;;
    ( "bin-"*"-freebsd" ) echo "sh"   ;;
    ( "bin-"*"-solaris" ) echo "sh"   ;;
    ( "bin-"*"-darwin"  ) echo "sh"   ;;
    ( "bin-"*"-osx-mac" ) echo "idmg" ;;
    ( "bin-"*"-win32"   ) echo "exe"  ;;
    ( * ) exit_error "Unknown dist type+platform for" \
                     "installer_of_dist_type_platform: \"$1\"" ;;
  esac
}
explanation_of_installer_type() {
  case "$1" in
    ( "tgz"  ) echo "Unpack this file using" \
                    "\"gunzip <file> | tar xvf -\"." ;;
    ( "dmg"  ) echo "Mount this disk image and copy the Racket folder to" \
                    "your disk." ;;
    ( "idmg" ) echo "Some browsers will automatically mount & copy the" \
                    "\"Racket\" folder to your desktop; if yours does not," \
                    "mount the disk and copy it yourself." ;;
    ( "zip"  ) echo "Use unzip to extract the Racket folder to your disk." ;;
    ( "sh"   ) echo "Execute this file with \"sh <file>\"," \
                    "and follow the instructions." ;;
    ( "exe"  ) echo "This is a standard Windows installer." ;;
    ( * ) exit_error "Unknown installer type for" \
                     "explanation_of_installer_type: \"$1\"." ;;
  esac
}

# This is for running racket scripts, unrelated to the build itself
export PLTHOME="$maindir/$installdir" \
       PLT_EXTENSION_LIB_PATHS="" \
       PLTPLANETDIR="$tmpdir/planet"
export PATH="$PLTHOME/bin:$PATH"
unset PLTCOLLECTS; export PLTCOLLECTS

# useful for tests etc
export PLT_BUILD="yes"

# setup for gui tests (and outside of them, there will not be a :65
# display, so trying any gui will fail)
real_DISPLAY="$DISPLAY"
export DISPLAY=":65"
if [[ "$XAUTHORITY" = "" ]]; then export XAUTHORITY="$HOME/.Xauthority"; fi

###############################################################################
### Utilities

NL=$'\n'

no_exit_on_error="no"
exit_error() {
  echo ""
  echo "<<<BOOM>>>  (Working on $machine($platform))" 1>&2
  echo "$*" 1>&2
  if [[ "$no_exit_on_error" = "yes" ]]; then
    echo ""
  else
    echo "Aborting" 1>&2
    write_status "Build error, aborted: $*"
    exit 1
  fi
}
dont_exit() {
  no_exit_on_error="yes" ; "$@" ; no_exit_on_error="no"
}

cleanup_run_files() {
  rm -f "$lockfile"
  rm -f "$statusfile_last"
  if [[ -e "$statusfile" ]]; then mv "$statusfile" "$statusfile_last"; fi
}

# Utilities for multi-level variables that can be used as sort of an
# associative arrays, with names that are treated similarly to paths and a
# default context similar to the current directory.  (Implemented as plain
# variables, using "__" as the translation of "/" level separators.)
mcontext="/" # the current context for m-ops
mset() {
  # mset goes over all args, which can have the following shapes:
  #   ...=...  sets a variable in the current context
  #   /.../... sets the current absolute context
  #   .../...  sets the current relative context
  local m mvar val
  for m; do
    case "$m" in
      ( *=* ) mvar="${m%%=*}" val="${m#*=}"
              normalize_mvar; obfuscate_mvar
              eval "${mvar}=\"${val}\""
              ;;
      ( */* ) mvar="$m"; normalize_mvar; mcontext="$mvar" ;;
      ( *   ) exit_error "unknown name in mset: $m" ;;
    esac
  done
}
mget() {
  # mget crawls over all args, and for each one retreives the mvar into a plain
  # variable.  The full form of an arg is "tgt=mvar?def" for a specified target
  # var (default is the mvar's basename), and a default.  The default can start
  # with `@' to make it another mvar reference
  local m mvar tgt def nodef=" <<<NO---DEFAULT>>> "
  for m; do
    mvar=""; tgt=""; def="$nodef"
    if [[ "$m" = *=*   ]]; then tgt="${m%%=*}";  m="${m#*=}";    fi
    if [[ "$m" = *"?"* ]]; then def="${m#*[?]}"; m="${m%%[?]*}"; fi
    mvar="$m"; normalize_mvar
    if [[ "$tgt" = ""  ]]; then tgt="${mvar##*/}"; fi
    obfuscate_mvar
    if [[ "$def" = "$nodef" ]]; then
      eval "${tgt}=\"\${${mvar}?${m} is not set}\""
    else
      local R="$nodef"
      eval "R=\"\${${mvar}:-\"$R\"}\""
      if [[ "$R" != "$nodef" ]]; then eval "${tgt}=\"${R}\""
      elif [[ "$def" = "@"* ]];  then mget "${tgt}=${def#@}"
      else                            eval "${tgt}=\"${def}\""
      fi
    fi
  done
}
machineget() {
  # an mget-like version for machines, using the default fields (and a global
  # $machine value)
  local m tgt
  for m; do
    if [[ "$m" = *=* ]]; then tgt="${m%%=*}="; m="${m#*=}"; else tgt=""; fi
    mget "${tgt}/machines/${machine}/${m}?@/machines/D/${m}"
  done
}
# Utility for the above: normalize `mvar' (mvar and mcontext are globals)
normalize_mvar() {
  # absolute mvar => don't use the mcontext
  if [[ ! "$mvar" = "/"* ]]; then mvar="/${mcontext}/${mvar}"; fi
  mvar="${mvar}/"             # add "/" suffix for the processing below
  mvar="${mvar//\/+(\/)//}"   # "//" -> "/"
  mvar="${mvar//\/.\///}"     # "/./" -> "/"
  mvar="${mvar//\/+([^\/])\/..\///}" # eliminate ".."
  mvar="${mvar/#\/+(..\/)//}" # eliminate prefix ".."
  mvar="${mvar%/}"            # remove "/" suffix
}
obfuscate_mvar() {
  mvar="${mvar//\//__}"
  mvar="${mvar//-/_}"
}
# now that we have these functions, do the initial_msets
mset "${initial_msets[@]}"
# global build-context variables, and set main-machine values
machine="$workmachine"
machineget platform workdir

# portable `echo -n'
if [[ "$(echo -n)" = "-n" ]]; then
  echo_n() { echo ${1+"$@"}"\c"; }
else
  echo_n() { echo -n ${1+"$@"}; }
fi

show() {
  if [[ "x$1" = "x-s" ]]; then shift; write_status "$*"; fi
  if [[ "$verbose" = "yes" ]]; then
    echo ""
    # It would be nice to use something like
    #   echo ">>> $*" | fmt -p '>>>' -w 79
    # but on osx fmt doesn't do that kind of stuff
    echo ">>> $*"
  fi
}

# a yes/no question mode for some vars, possibly set a constant answer
ask_mode="no"
ask_answers=""
fixed_reply=""
is_yes() {
  local var="$1"; shift
  local val; eval val="\$$var"
  local reply
  if [[ "$val" = "yes" ]]; then return 0
  elif [[ "$val" = "no" ]]; then return 1
  elif [[ "$val" = "ask_or_yes" ]]; then
    if [[ "$ask_mode" = "yes" ]]; then
      echo "" 1>&2
      echo "" 1>&2
      while true; do
        echo_n ">>> QUESTION >>> $var [y/n/Y/N] ? " 1>&2
        if [[ "$fixed_reply" != "" ]]; then reply="$fixed_reply"
        elif [[ "x$ask_answers" != "x" ]]; then
          reply="${ask_answers:0:1}"
          ask_answers="${ask_answers:1}"
        else read -sn 1 reply; fi
        echo "$reply" 1>&2
        case "$reply" in
          ( Y ) fixed_reply="y"; reply="y" ;;
          ( N ) fixed_reply="n"; reply="n" ;;
        esac
        case "$reply" in
          ( y ) eval $var="yes"; return 0 ;;
          ( n ) eval $var="no";  return 1 ;;
          ( * ) reply="" ;;
        esac
      done
    else
      eval $var="yes"; return 0
    fi
  else
    exit_error "bad value for flag '$var': '$val'"
  fi
}

lookfor() {
  save_IFS="${IFS}"
  IFS="${IFS}:"
  for dir in $PATH; do
    if test -x "$dir/$1"; then
      IFS="$save_IFS"
      echo_n "$dir/$1"
      return
    fi
  done
  IFS="$save_IFS"
}

_run() {
  show "Running \"$*\""
  "$@" \
  || exit_error "Errors when running \"$*\""
}

# there is a common sh hack for getting the Nth word from a command:
#   "... $(set $(blah); echo $1) ..."
# the problem with this is if blah produces no output -- which will end up
# dumping out the complete environment -- so use this instead
__get_first_arg() { printf '%s' "$1"; }
__get_first_output() { __get_first_arg $(cat); }
# inputs: command to run
get_first() { "$@" | __get_first_output; }

_cd() {
  local OLDWD="$(pwd)"
  cd "$1" || exit_error "Could not cd into \"$1\""
  local NEWWD="$(pwd)"
  if [[ "$NEWWD" != "$OLDWD" ]]; then
    show "Now in \"$(pwd)\""
  fi
}

_md() {
  for x; do
    if [[ ! -d "$x" ]]; then
      show "Creating directory \"$x\""
      mkdir -p "$1" || exit_error "Could create directory \"$x\""
    fi
  done
}

_mcd() {
  _md "$1"; _cd "$1"
}

_rm() {
  for x; do
    if [[ -h "$x" ]]; then
      show "Deleting link \"$x\""
      rm -f "$x" || exit_error "The \"$x\" link cannot be deleted"
    elif [[ -d "$x" ]]; then
      show "Deleting directory \"$x\""
      rm -rf "$x" || exit_error "The \"$x\" directory cannot be deleted"
    elif [[ -e "$x" ]]; then
      show "Deleting \"$x\""
      rm -rf "$x" || exit_error "\"$x\" cannot be deleted"
    fi
  done
}

_rmd() {
  _rm "$1"; _md "$1"
}

_rmcd() {
  _rm "$1"; _mcd "$1"
}

_mv() {
  show "Moving \"$*\""
  mv "$@" || exit_error "Could not move \"$*\""
}

_cat() {
  show "Showing \"$@\""
  cat "$@" || exit_error "Could not show \"$@\""
}

_cp() {
  show "Copying: \"$*\""
  cp -p "$@" || exit_error "Could not copy \"$*\""
}

_scp() {
  show "Copying: \"$*\""
  scp -p "$@" || exit_error "Could not copy \"$*\""
}

_ln() {
  show "SymLinking \"$2\" -> \"$1\""
  ln -s "$1" "$2" || exit_error "Could not symlink \"$2\"->\"$1\""
}

_zip() {
  local zip_file="$1"; shift
  show "Zipping \"$*\" to \"$zip_file\" in \"$(pwd)\""
  zip -qr9 "$zip_file" "$@" \
  || exit_error "Could not zip \"$*\" to \"$zip_file\" in \"$(pwd)\""
}

# try to use gtar if we can find it
TAR="$(lookfor gtar)"
if [[ "$TAR" = "" ]]; then TAR="$(lookfor tar)"; fi

_tar() {
  local tar_file="$1"; shift
  show "Tarring \"$*\" to \"$tar_file\" in \"$(pwd)\""
  "$TAR" cf "$tar_file" "$@" \
  || exit_error "Could not tar \"$*\" to \"$tar_file\" in \"$(pwd)\""
}

_tgzip() {
  local tgz_file="$1"; shift
  show "Packing \"$*\" to \"$tgz_file\" in \"$(pwd)\""
  "$TAR" czf "$tgz_file" "$@" \
  || exit_error "Could not pack \"$*\" to \"$tgz_file\" in \"$(pwd)\""
}

_tar_add() {
  local tar_file="$1"; shift
  show "Adding \"$*\" to \"$tar_file\" in \"$(pwd)\""
  "$TAR" uf "$tar_file" "$@" \
  || exit_error "Could not add \"$*\" to \"$tar_file\" in \"$(pwd)\""
}

_tgunzip() {
  show "Unpacking \"$1\" in \"$(pwd)\""
  "$TAR" xzf "$1" || exit_error "Could not unpack \"$1\" in \"$(pwd)\""
}

_tgunzipm() {
  show "Unpacking \"$1\" in \"$(pwd)\""
  "$TAR" xzmf "$1" || exit_error "Could not unpack \"$1\" in \"$(pwd)\""
}

_strip() {
  local f
  for f; do
    if [[ -e "$f" ]]; then
      show "Stripping \"$f\""
      strip -S "$f" || exit_error "Could not strip \"$f\""
    fi
  done
}

git_get() { # inputs: git repository, git branch, path in $maindir
  local repo="$1" branch="$2" dir="$3"; shift 3
  show "Getting $repo/$branch to $maindir/$dir"
  _cd "$maindir"
  if [[ ! -d "$dir" ]]; then
    _run git clone "git://git.racket-lang.org/$repo.git" "$dir"
  fi
  _cd "$dir"
  _run git checkout "$branch"
  _run git pull --ff-only --all
  git status -s > "$tmpdir/git-st" || exit_error "problems running git status"
  if [[ -s "$tmpdir/git-st" ]]; then
    echo "------------"
    cat "$tmpdir/git-st" 1>&2
    rm -f "$tmpdir/git-st"
    exit_error "The clean directory is not clean (see above)"
  fi
  rm -f "$tmpdir/git-st"
  _cd "$maindir"
}

base_status=""
machine_suffix=""
write_status() {
  local message="${*}${machine_suffix}"
  if [[ "$gitbranch" != "master" ]]; then
    message="($gitbranch build) $message"
  fi
  if [[ "x$machine_suffix" = "x" ]]; then
    if [[ ! -e "$statusfile" ]]; then
      touch "$statusfile"
      chcon --type=httpd_sys_content_t "$statusfile"
    fi
    echo "$message" > "$statusfile"
  else
    # greppable lines for status, filtered out in final log (see below)
    echo "### <<< $message >>>"
  fi
}

header() {
  local status=""
  case "x$1" in
    ( "x-s" ) shift; status="$*"; base_status="$*";;
    ( "x+s" ) shift; status="$base_status, $*";;
  esac
  local message="${*}${machine_suffix}"
  local line="============================================================"
  local message_len=${#message}
  local idx1=$(( ( 77 - $message_len ) / 2 ))
  local idx2=$(( ( 78 - $message_len ) / 2 ))
  local line1=${line:0:$(( ( $idx1 < 3 ) ? 3 : $idx1 ))}
  local line2=${line:0:$(( ( $idx2 < 3 ) ? 3 : $idx2 ))}
  local dashes="$(echo "$line1 $message $line2" | sed 's/./-/g')"
  echo ""
  echo ""
  echo "$dashes"
  echo "$line1 $message $line2"
  echo "$dashes"
  if [[ "x$status" != "x" ]]; then write_status "$status"; fi
  echo ""
}

build_step() { # inputs: name, command
  local jobname="$1"; shift
  header -s "Building: $jobname"
  show "Running \"$*\""
  start_timer
  "$@" || exit_error "\"$jobname\" part of build process failed"
  show_time "--==> $jobname on $machine($platform) done,"
}

cur_secs() {
  date '+%s'
}
start_timer() {
  timer_start="$(cur_secs)"
}
show_time() {
  local time=$(( $(cur_secs) - $timer_start ))
  local secs=$(( $time % 60 ))
  local mins=$(( $time / 60 ))
  show "$1 time: $(printf '%d:%02d' $mins $secs)"
}

choose_for_testing() { # input: test_mode, options ...
  # choose items from the given inputs, either all, the first, or a random one
  local mode="$1"; shift
  case "$mode" in
    ( all ) echo "$*" ;;
    ( def ) echo "$1" ;;
    ( rnd ) mode=$(( $RANDOM % $# + 1 )); echo "${!mode}" ;;
    ( * ) exit_error "bad value in choose_for_testing: $mode"
  esac
}

# Utilities for GUI tests (and process management)

_kill() { # args: pid [process name]
  local pid="$1"; shift
  local desc="$pid"
  if [[ "$1" != "" ]]; then desc="$1 ($pid)"; shift; fi
                                     if [[ ! -d "/proc/$pid" ]]; then return; fi
  show "Killing $desc"
  kill -15 "$pid" > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi
  sleep 0.5                        ; if [[ ! -d "/proc/$pid" ]]; then return; fi
  sleep 0.5                        ; if [[ ! -d "/proc/$pid" ]]; then return; fi
  sleep 1                          ; if [[ ! -d "/proc/$pid" ]]; then return; fi
  echo "re-killing $desc"
  kill -15 "$pid" > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi
  sleep 2                          ; if [[ ! -d "/proc/$pid" ]]; then return; fi
  echo "re-re-killing $desc"
  kill -15 "$pid" > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi
  sleep 2                          ; if [[ ! -d "/proc/$pid" ]]; then return; fi
  echo "murdering $desc"
  kill -9 "$pid"  > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi
  sleep 2                          ; if [[ ! -d "/proc/$pid" ]]; then return; fi
  echo "re-murdering $desc"
  kill -9 "$pid"  > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi
  sleep 2                          ; if [[ ! -d "/proc/$pid" ]]; then return; fi
  echo "re-re-murdering $desc"
  kill -9 "$pid"  > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi
  sleep 2                          ; if [[ ! -d "/proc/$pid" ]]; then return; fi
  echo "BOOM Zombie alert: $desc did not die"
}

_timeout_run() { # first input is the timeout
  local timeout="$1"; shift
  local exe="$1"
  show "Running \"$*\" with a timeout of $timeout"
  "$@" &
  local pid="$!"
  local result="99"
  ( # sleep in background so we're still interruptible
    local sleeper="$$"
    alldone() { kill -15 "$sleeper"; exit; }
    trap alldone 0 3 9 15
    sleep "$timeout" &
    sleeper="$!"
    wait "$sleeper"
    _kill "$pid" "$exe [timeout]"
    ) &
  local killerpid="$!"
  wait "$pid"; result="$?"
  _kill "$killerpid"
  if [[ "$result" != "0" ]]; then
    exit_error "Errors when running \"$*\" (with a timeout)"
  fi
  return "$result"
}

Xvncpid=""
Xwmpid=""
_start_xvnc() {
  local xvnclog="$tmpdir/xvnc-log"
  show "Starting Xvnc (logfile at \"$xvnclog\")"
  # Create Xauth cookie
  cookie="$(mcookie)"
  xauth -f "$XAUTHORITY" add "$(uname -n)$DISPLAY" . "$cookie"
  xauth -f "$XAUTHORITY" add "$(uname -n)/unix$DISPLAY" . "$cookie"
  # Create Xvnc session, with a WM
  Xvnc "$DISPLAY" \
    -rfbport 6565 \
    -localhost \
    -desktop "Racket-Session" \
    -geometry 1024x768 \
    -depth 16 \
    -httpPort=0 \
    -auth "$XAUTHORITY" \
    -rfbauth "$HOME/.vnc/passwd" \
    -br \
    > "$xvnclog" 2>&1 &
  Xvncpid="$!"; sleep 0.5
  echo "Xvnc running ($Xvncpid)"
  metacity --sm-disable &
  Xwmpid="$!"; sleep 0.5
  echo "window manager running ($Xwmpid)"
  # to see the window, uncomment this
  # DISPLAY="$real_DISPLAY" vncviewer ::6565 -PasswordFile "$HOME/.vnc/passwd" &
}
_end_xvnc() {
  show "Killing Xvnc session"
  if [[ "$Xvncpid" = "" ]]; then show "Xvnc was not started"; return 1; fi
  _kill "$Xwmpid" "window manager"
  _kill "$Xvncpid" "Xvnc"
  Xvncpid=""; Xwmpid=""
}

parse_c_define() { # input: filename, varname
  local file="$1" varname="$2"; shift 2
  grep "^ *# *define * $varname * " "$file" \
  | sed -e 's/^ *# *define * [^ ]* * //' -e 's/ * $//'
}

version_init() { # input: plthome
  local vfile="$1/src/racket/src/schvers.h"
  [[ -e "$vfile" ]] || exit_error "Could not find version file at \"$vfile\""
  # parse version info
  version="$(parse_c_define "$vfile" MZSCHEME_VERSION | sed -e 's/\"//g')"
  version1="$(parse_c_define "$vfile" MZSCHEME_VERSION_X)"
  version2="$(parse_c_define "$vfile" MZSCHEME_VERSION_Y)"
  version3="$(parse_c_define "$vfile" MZSCHEME_VERSION_Z)"
  version4="$(parse_c_define "$vfile" MZSCHEME_VERSION_W)"
  # consistency check
  local VER="$version1.$version2"
  if [[ "$version4" != "0" ]]; then VER="$VER.$version3.$version4"
  elif [[ "$version3" != "0" ]]; then VER="$VER.$version3"
  fi
  [[ "$version" = "$VER" ]] \
    || exit_error "Mismatch in \"$vfile\": $version vs $VER"
  # release is when the last one is zero
  if [[ "$version4" = "0" ]]; then
    header "This is a release version ($version)"
    releasing="yes"
    reallyreleasing="yes"
  elif [[ "$gitbranch" = "release" ]]; then
    header "This is a pre-release version ($version)"
    releasing="yes"
    reallyreleasing="no"
  else
    header "This is a non-release version ($version)"
    releasing="no"
    reallyreleasing="no"
  fi
}

# html functions -- all write to $htmloutput
# ($htmloutput is usually $index in the current directory)
html_begin() { # inputs: title [output-name]
  local htmltitle="$1"; shift
  saved_htmloutput="$htmloutput$NL$saved_htmloutput"
  htmloutput="$index"
  if [[ "$1" != "" ]]; then htmloutput="$1"; shift; fi
  if [[ "$htmloutput" != /* ]]; then htmloutput="$(pwd)/$htmloutput"; fi
  show "Creating \"$htmloutput\" for \"$htmltitle\""
  # the *.title. file marks this for later patching through the web templates,
  # so it should be created even if it won't get used later.
  _rm "$htmloutput" "$htmloutput.title."
  echo "$htmltitle" > "$htmloutput.title."
}
html_end() {
  show "Finished \"$(pwd)/$htmloutput\""
  htmloutput="${saved_htmloutput%%$NL*}"
  saved_htmloutput="${saved_htmloutput#*$NL}"
}
html_table_begin() { # inputs: [rules-attr]
  local rules="rows"
  if [[ "$1" != "" ]]; then rows="$1"; fi
  { echo "<blockquote>"
    echo "<table border=\"1\" frame=\"void\" rules=\"$rules\"" \
         "cellspacing=\"0\">"
  } >> "$htmloutput"
}
html_show() { # inputs: <string> or <-f file> ...
  { while [[ "$#" -gt "0" ]]; do
      if [[ "$1" = "-f" ]]; then shift; cat "$1"; else echo "$1"; fi
      shift
    done
  } >> "$htmloutput"
}
html_file_row() { # inputs: filename, explanation ...
  local fname="$1"; shift
  { echo_n "<tr><td><nobr>&bull;&nbsp;"
    echo_n "<a href=\"$fname\"><tt>$fname</tt></a></nobr>"
    if [[ -f "$fname" ]]; then
      local size="$(get_first du -h "$fname")"
      if [[ "$size" = *[MG] ]]; then echo_n "&nbsp;<small>($size)</small>"; fi
    fi
    echo "</td>"
    echo "<td>&nbsp;</td><td>$*</td></tr>"
  } >> "$htmloutput"
}
html_table_end() {
  echo "</table></blockquote>" >> "$htmloutput"
}

run_part() {
  local exec=no
  local bg=no
  while true; do
    case "$1" in
      ( -exec ) exec="yes"; shift; continue ;;
      ( -bg   ) bg="yes";   shift; continue ;;
      ( * ) break ;;
    esac
  done
  local runhost="$1" runpart="$2"; shift 2
  # echo "runhost=$runhost, exec=$exec, bg=$bg, $*"
  if [[ "$runhost" = "$hostname" ]]; then
    if [[ "$bg" = "yes" ]]; then "$runpart" "$@" &
    # must check $exec before running -- since if this is done in bg, then
    # other calls to this function will overwrite it!
    elif [[ "$exec" = "yes" ]]; then "$runpart" "$@"; exit $?
    else "$runpart" "$@"
    fi
  else
    # ssh does not preserve proper arguments, so this does not work with
    # arguments that contain spaces.
    local rbuild="$remotebuildscript"
    _scp "$buildscript" "${runhost}:$remotebuildscript"
    local ssh_vars_vals i var val
    i=0
    while [[ "$i" -lt ${#ssh_vars[*]} ]]; do
      var="${ssh_vars[i]}"
      eval "val=\"\${$var}\""
      ssh_vars_vals[$i]="${var}=${val}"
      i=$((i+1))
    done
    local cmd
    cmd="--dispatch ${ssh_vars_vals[@]} $runhost $runpart"
    if [[ "$exec" = "yes" ]]; then
      exec ssh -x "$runhost" "$rbuild" $cmd "$@" \
        || exit_error "Errors running \"$rbuild\" on \"$runhost\""
      exit_error "Something is wrong with \"exec\""
    elif [[ "$bg" = "yes" ]]; then
      ssh -x "$runhost" "$rbuild" $cmd "$@" &
    else
      ssh -x "$runhost" "$rbuild" $cmd "$@" \
        || exit_error "Errors running \"$rbuild\" on \"$runhost\""
    fi
  fi
}


###############################################################################
### Build Parts

MAIN() {
  # switch to build machine, if invoked remotely
  run_part -exec "$workmachine" MAIN_BUILD "$@"
}

## ============================================================================

MAIN_BUILD() {

  ## --------------------------------------------------------------------------
  # use a lock file, no retries, and recreate it if it's over 3 hours old
  _run lockfile -r 0 -l 10800 "$lockfile"
  trap cleanup_run_files 0 3 9 15

  # cleanup old bg logs early so the status won't show bogus leftovers
  _rm "$bglogfile-"*

  ## --------------------------------------------------------------------------
  header -s "Begin ($(date +'%Y-%m-%d %H:%M'))"

  timestamp="$(date '+%Y%m%d%H%M')"
  htmltimestamp="$(date '+updated at %A, %B %d %Y, %H:%M %Z')"
  if [[ "$1" = "ask" ]]; then
    ask_mode="yes"; shift
    if [[ "$1" = +([yYnN ]) ]]; then
      ask_answers="$(echo "$1" | tr -d ' ')"; shift
    fi
  fi

  ## --------------------------------------------------------------------------
  if is_yes make_repo; then
    header -s "Repository updates"
    git_get "plt" "$gitbranch" "$cleandir"
    git_get "iplt" "$gitibranch" "$internaldir"
  else
    show "Skipping repository updates"
  fi

  version_init "$maindir/$cleandir"
  if is_yes make_repo; then
    DO_AUTO_UPDATES
  fi

  if is_yes make_bins; then
    header -s "Creating source archive"
    _cd "$maindir"
    _rm "$repotgz"
    _cd "$maindir/$cleandir"
    git archive --format=tar "$gitbranch" | gzip > "$repotgz" \
      || exit_error "Could not create archive"
    git archive --format=tar --prefix=racket/ "$gitbranch" \
    | gzip > "$maindir/$cleantgz" \
      || exit_error "Could not create archive"
    _cd "$maindir"
  fi

  # send build notification message
  if [[ "$buildnotifyemail" != "" && "$CRON" != "yes" ]]; then
    show "Sending notifications"
    echo "Build starting at $(date +'%Y-%m-%d %H:%M')" \
    | mail -s "A build is starting..." "$buildnotifyemail"
  fi

  ## --------------------------------------------------------------------------
  header -s "Dispatching build jobs"

  local m
  if is_yes make_bins; then
    for m in "${machines[@]}"; do DO_COPY_BUILD "$m"; done
  else
    show "Skipping binaries"
  fi

  # build web pages and pdfs while other machines continue doing their builds
  BUILD_WEB
  BUILD_DOCS_AND_PDFS

  # and now wait for all builds
  if is_yes make_bins; then
    show -s "Waiting for remote jobs to finish"
    wait
    for m in "${machines[@]}"; do
      machine="$m"
      machineget mplatform=platform
      if [[ "$machine" != "$workmachine" ]]; then
        header "{{{ Building ${machine}(${mplatform}) remotely }}}"
        _cat "$bglogfile-$machine" | grep -v "^### <<< .* >>>"
        _rm "$bglogfile-$machine"
        _rm "$curlog_prefix-$(basename "$bglogfile-$machine")"
      fi
    done
  fi

  if is_yes make_builddirs; then COPY_AND_BUILD_BINARY_DIRS
  else show "Skipping copying and dirs"; fi

  if is_yes make_bundles; then BUILD_BUNDLES
  else show "Skipping bundles"; fi

  if is_yes make_installers; then BUILD_INSTALLERS
  else show "Skipping installers"; fi

  if is_yes make_pre_web; then BUILD_PRE_WEB; fi

  _rm "$lockfile"

  header -s "Done ($(date +'%Y-%m-%d %H:%M'))"

}

## ============================================================================

DO_AUTO_UPDATES() {

  ## --------------------------------------------------------------------------
  header -s "Updating version-dependent files"

  _cd "$maindir/$cleandir"

  # if the racket executable or the script is not there, we'll fail, but that
  # shouldn't be a problem since it will run again next time
  if [[ -x "$PLTHOME/bin/racket" && -x "$PLTHOME/$versionpatcher" ]]; then
    dont_exit _run "$PLTHOME/$versionpatcher" "$version"
  else
    show "Skipping version update (no racket executable or version script)"
  fi

  if [[ "$(git status -s | wc -l)" != 0 ]]; then
    _run git commit -am "New Racket version $version."
    _run git push git@git.racket-lang.org:plt "$gitbranch"
  fi

}

## ============================================================================

DO_COPY_BUILD() { # inputs -- machine-name (for ssh)

  ## --------------------------------------------------------------------------
  machine="$1"; shift

  if [[ "$machine" != "$workmachine" ]]; then
    show "Running DO_BUILD on $machine in the background"
    _run ssh "$machine" \
      "rm -rf \"$tmpdir\"; mkdir -p \"$tmpdir\"; cat > \"$repotgz\"" \
      < "$repotgz"
    touch "$bglogfile-$machine"
    chcon --type=httpd_sys_content_t "$bglogfile-$machine"
    _rm "$curlog_prefix-$(basename "$bglogfile-$machine")"
    _ln "$bglogfile-$machine" "$curlog_prefix-$(basename "$bglogfile-$machine")"
    run_part -bg "$machine" "DO_BUILD" "$releasing" "$@" \
             &> "$bglogfile-$machine"
  else
    header "{{{ Doing $machine($platform) locally }}}"
    run_part "$machine" "DO_BUILD" "$releasing" "$@"
  fi

}

## ============================================================================

DO_BUILD() { # inputs -- releasing
  ## --------------------------------------------------------------------------
  releasing="$1"; shift
  machineget platform workdir moveto copytobak \
             configure_args ext_lib_paths renice more_setup_args test_gui
  if [[ "${machine:-$workmachine}" != "$workmachine" ]]; then
    if [[ "$platform" = "" ]]; then machine_suffix=" [$machine]"
    else machine_suffix=" [$machine($platform)]"; fi
  fi

  header -s "Starting build"

  local machine_name="$(uname -n)"
  if [[ "$machine_name" != *.* ]]; then
    if [[ "$(uname -o)" = "Cygwin" ]]; then
      machine_name="$machine_name ($(ipconfig | \
                      grep "Connection-specific DNS Suffix" | \
                      sed -e 's/^[^:]*: *//' | \
                      sort -u | grep "[.]"))"
    else
      machine_name="$(hostname -f)"
    fi
  fi
  show "Machine identifies as $machine_name: $(uname -a)"

  if [[ "$renice" != "" ]]; then dont_exit _run renice "$renice" "$$"; fi

  export PLTHOME="$workdir/$installdir" PATH="$PLTHOME/bin:$PATH"
  export SETUP_ARGS="-N raco -l- raco setup -j 1 -U"

  # make sure we don't use any planet caches (PLTPLANETDIR is set globally)
  _rm "$PLTPLANETDIR"

  if [[ "$releasing" = "yes" ]]; then
    # don't do this for the nightly builds -- if they fail and a previous tgz
    # is there, we'll end up using it
    _rm "$fulltgz"
  fi

  if [[ ext_lib_paths != "" ]]; then
    export PLT_EXTENSION_LIB_PATHS="${ext_lib_paths}:$PLT_EXTENSION_LIB_PATHS"
  fi

  header -s "Unpacking source tree"

  _rmcd "$PLTHOME"
  _tgunzipm "$repotgz"
  if [[ "$machine" != "$workmachine" ]]; then
    # don't waste time on the docs except on the main machine
    export SETUP_ARGS="$SETUP_ARGS -D"
  fi

  # additional setup args
  if [[ "x$more_setup_args" != "x" ]]; then
    export SETUP_ARGS="$SETUP_ARGS $more_setup_args"
  fi

  ## --------------------------------------------------------------------------
  if [[ "$platform" = *"-win32" ]]; then
    export PLTPLANETDIR="$(cygpath -w "$PLTPLANETDIR")"
    DO_WINDOWS_BUILD
  else
    _mcd "$PLTHOME/src/build"
    machineget LDFLAGS; export LDFLAGS
    # use --disable-libffi so we get the internal copy to build and link
    # since not all distros have libffi
    build_step "configure" ../configure --disable-libffi ${configure_args}
    build_step "make both" make both
    build_step "make install" make plain-install-both
    build_step "raco setup" "$PLTHOME/bin/racket" $SETUP_ARGS
  fi

  ## --------------------------------------------------------------------------
  header -s "Stripping binaries"

  # Strip binaries
  _cd "$PLTHOME"
  case "$platform" in
    ( *"-linux"* | *"-freebsd" | "sparc-solaris" | *"-darwin" )
      _strip "bin/racket"{,3m,cgc} "bin/gracket"{,3m,cgc}
      ;;
    ( *"-osx-mac" )
      _strip "bin/racket"{,3m,cgc} "GRacket"*".app/Contents/MacOS/GRacket"* \
             "lib"/{,G}"Racket.framework"/"Versions"/*/{,G}"Racket"
      ;;
    ( *"-win32" )
      # (just don't include *.pdb and *ilk)
      show "Nothing to strip for \"$platform\""
      ;;
    ( * )
      exit_error "don't know if binaries for $platform should be stripped"
      ;;
  esac

  ## --------------------------------------------------------------------------
  header -s "Creating \"$fulltgz\""

  _rm "$fulltgz"
  _cd "$workdir"
  # excluding x/y does not work on solaris, so rename it instead
  _mv "$PLTHOME/src" "$PLTHOME/___src___"
  _tgzip "$fulltgz" --exclude="___src___" \
         --exclude="*.[Pp][Dd][Bb]" --exclude="*.[Ii][Ll][Kk]" \
         "$installdir"
  _mv "$PLTHOME/___src___" "$PLTHOME/src"

  ## --------------------------------------------------------------------------
  # choose a test mode (def/rnd/all)
  local test_mode="def"
  if [[ "$run_all_tests" = "yes" ]];     then test_mode="all";
  elif [[ "$releasing" = "yes" ]];       then test_mode="all";
  elif [[ "$(( $RANDOM % 2 ))" = "0" ]]; then test_mode="rnd";
  fi
  header -s "Testing Racket ($test_mode)"
  local testdir="$tmpdir/tests"
  _rmcd "$testdir"

  local _exe _jit exe flags
  for _exe in $(choose_for_testing $test_mode 3m cgc); do
    for _jit in $(choose_for_testing $test_mode yes no); do
      if [[ "${_exe}" = "cgc" ]]; then exe="cgc"; else exe=""; fi
      if [[ "$platform" = *"-win32" ]]; then
        exe="$PLTHOME/Racket$exe.exe"
      else
        exe="$PLTHOME/bin/racket$exe"
      fi
      flags=""
      if [[ "${_jit}" = "no" ]]; then flags="--no-jit $flags"; fi
      dont_exit _run env HOME="$testdir" DISPLAY="" \
        "$exe" $flags "$PLTHOME/collects/tests/run-automated-tests.rkt"
    done
  done
  sleep 8 # time to flush stderr

  # GRacket-based tests on the main machine, in an Xvnc session
  if [[ "$test_gui" = "yes" ]]; then
    header -s "Testing DrRacket"
    if [[ "$platform" = *"-win32" ]]; then
      dont_exit "$PLTHOME/gracket.exe" "$(cygpath -w "$PLTHOME/$drtestscript")"
    else
      _start_xvnc
      dont_exit env HOME="$testdir" "$PLTHOME/$drtestscript"
      _end_xvnc
    fi
  fi

  ## --------------------------------------------------------------------------
  # move to the target at the end of the build, only if building from master
  local targetdir="" mode="" op=""
  if [[ "$gitbranch" != "master" ]]; then :
  elif [[ "$copytobak" != "" ]]; then
    targetdir="$copytobak"; mode="bk"; op="Backing up"
  elif [[ "$moveto" != "" ]]; then
    targetdir="$moveto"; mode="mv"; op="Moving"
  fi
  if [[ "$targetdir" != "" ]]; then
    header -s "$op installation to \"$targetdir\""
    _md "$targetdir/$installdir-new"
    _cd "$workdir/$installdir"
    show "Copying \"$PLTHOME\" to \"$targetdir/$installdir-new\""
    "$TAR" cf - . | ( cd "$targetdir/$installdir-new"; "$TAR" xf - ) \
      || exit_error \
         "Could not copy \"$PLTHOME\" to \"$targetdir/$installdir-new\""
    _cd "$targetdir"
    if [[ "$mode" = "mv" ]]; then
      # move the installation, trying to delete the previous one if possible
      # do it this way in case there is already a leftover "$installdir-old"
      _md "$installdir-old"
      _mv "$installdir" "$installdir-old/old-$(date '+%Y%m%d%H%M')-$$"
      _mv "$installdir-new" "$installdir"
      _rm "$PLTHOME"
      show "Removing \"$targetdir/$installdir-old\""
      # this is done this way in case there is an old process using a binary
      # which will not allow removing the directory, but we don't care about
      # that.
      dont_exit _rm "$targetdir/$installdir-old"
    else
      # copy the installation to a backup directory, leaving one
      # backup of the old backup tree if it was there (this is used on
      # the build machine, so there's an updated copy of the tree at
      # ~scheme/racket); the main work directory is kept the same.
      if [[ -e "$installdir-backup" ]]; then _rm "$installdir-backup"; fi
      if [[ -e "$installdir" ]]; then _mv "$installdir" "$installdir-backup"; fi
      _mv "$installdir-new" "$installdir"
    fi
  fi

  ## --------------------------------------------------------------------------
  header -s "Done"

}

## ============================================================================

winpath2unix() { # input: windows path
  echo "$*" | sed 's_^\([a-zA-Z]\):[/\\]_/\1/_; s_\\_/_g'
}

# `grab_batch_vars' will run the given batch file and grab all of the
# environment variables it sets.
grab_one_batch_var() {
  if [[ "x$1" = "x" ]]; then return; fi
  # If this happens, then we'll need to quote 's as '"'"'s
  if [[ "$1" = *"'"* ]]; then echo "Terrible quote abort! $1"; exit 1; fi
  # echo "Setting ${1%%=*} := ${1#*=}"
  eval "export ${1%%=*}='${1#*=}'"
}
grab_batch_vars() { # input: batchfile args ...
  local exe="$1"; shift
  local tmpbatch="$tmpdir/grab_vars.bat"
  rm -f "$tmpbatch"
  cat >> "$tmpbatch" <<-EOF
	@echo off
	"$(cygpath -w "$(which env)")" > "$(cygpath -w "$tmpdir/E1")"
	call "$exe" $*
	"$(cygpath -w "$(which env)")" > "$(cygpath -w "$tmpdir/E2")"
EOF
  chmod +x "$tmpbatch"
  "$tmpbatch"
  rm "$tmpbatch"
  local a_file
  for a_file in "$tmpdir/E1" "$tmpdir/E2"; do
    sort "$a_file" | grep -v '^!' > "$a_file.s"; mv "$a_file.s" "$a_file"
  done
  local vars="$(comm -13 "$tmpdir/E1" "$tmpdir/E2")"
  rm "$tmpdir/E1" "$tmpdir/E2"
  while [[ "$vars" = *"$NL"* ]]; do
    grab_one_batch_var "${vars%%${NL}*}"
    vars="${vars#*${NL}}"
  done
  grab_one_batch_var "$vars"
}

win_build_step() { # inputs: type, name, [args...]
  header "Building: $2 ($1)"
  local btype="$1" bname="$2"; shift 2
  local arch="Win32"
  if [[ "$platform" = "x86_64-"* ]]; then arch="x64"; fi
  start_timer
  case "$btype" in
    ( "VSNET"   ) _cd "$PLTHOME/src/worksp/$bname"
                  _run "$VSNET" "$bname.sln" /build "Release|$arch"
                  ;;
    ( "NMAKE"   ) _run "$NMAKE" "$@"
                  ;;
    ( "RKTCGC"  ) _run "$PLTHOME/RacketCGC.exe" "$@"
                  ;;
    ( "RKT"     ) # prefer using no-suffix, then 3m, and then cgc
                  # (needed because cgc is used to build 3m)
                  local E="$PLTHOME/Racket"
                  if   [[ -x "${E}.exe"    ]]; then _run "${E}.exe" "$@"
                  elif [[ -x "${E}3m.exe"  ]]; then _run "${E}3m.exe" "$@"
                  elif [[ -x "${E}CGC.exe" ]]; then _run "${E}CGC.exe" "$@"
                  else exit_error "No Racket executable found"
                  fi
                  ;;
    ( "VSNET3M" ) _cd "$PLTHOME/src/worksp/$bname"
                  _run "$VSNET" "$bname.sln" /build "Release|$arch"
                  _run "$PLTHOME/Racket.exe" "xform.rkt" "$@"
                  _run "$VSNET" "$bname.sln" /build "3m|$arch"
                  ;;
    ( * ) exit_error "Unknown type for win_build_step: \"$btype\"" ;;
  esac
  show_time "--==> $bname on $machine($platform) done,"
}

DO_WINDOWS_BUILD() {

  ## --------------------------------------------------------------------------
  # make X: drives available as /X
  /usr/bin/mount -c /
  # ignore an occasional use of a dos-style file
  export CYGWIN="$CYGWIN nodosfilewarning"
  # needed for MS command-line utilities
  export PATH="$PATH:/c/Windows/system32"

  export TEMP="C:\\cygwin\\tmp" TMP="C:\\cygwin\\tmp"

  local arch="x86"
  if [[ "$platform" = "x86_64-"* ]]; then arch="x64"; fi

  # Note: commands must be executed using unix paths (also PATH)
  PROGS="C:\\Program Files"
  if [[ "$arch" = "x64" ]]; then PROGS="C:\\Program Files (x86)"; fi
  STUDIO="$PROGS\\Microsoft Visual Studio 10.0"
  VSNET="$(winpath2unix "$STUDIO\\Common7\\IDE\\devenv.com")"
  NMAKE="$(winpath2unix "$STUDIO\\VC\\bin\\nmake.exe")"

  grab_batch_vars "$STUDIO\\VC\\vcvarsall.bat" "$arch"

  PATH="$PATH:$(winpath2unix "$PLTHOME"):."

  export VSNET NMAKE PATH INCLUDE LIB

  # header -s "Windows: Convert .sln files"
  # local SAVED_IFS="$IFS"; IFS=$'\n'
  # local sln
  # for sln in $(find "$PLTHOME/src/worksp" -type f -name "*.sln"); do
  #   _cd "$(dirname "$sln")"
  #   _run "$VSNET" /upgrade "$(basename "$sln")"
  # done
  # IFS="$SAVED_IFS"

  header -s "Windows: Use new .sln files"
  local SAVED_IFS="$IFS"; IFS=$'\n'
  local file
  for file in $(find "$PLTHOME/src/worksp10" -type f); do
    _cp "$file" "${file//worksp10/worksp}"
  done
  IFS="$SAVED_IFS"

  header -s "Windows: Full build"
  win_build_step VSNET "racket"
  win_build_step VSNET "gracket"
  _cd "$PLTHOME/src/worksp/gc2"; win_build_step RKT "3M" make.rkt

  _cd "$PLTHOME"
  win_build_step VSNET "mzstart"
  win_build_step VSNET "mrstart"

  _cd "$PLTHOME/lib"
  win_build_step RKT "get-libs (core)" ../src/get-libs.rkt core
  win_build_step RKT "get-libs (gui)" ../src/get-libs.rkt gui
  win_build_step RKT "get-libs (db)" ../src/get-libs.rkt db
  win_build_step RKT "get-libs (com)" ../src/get-libs.rkt com

  header -s "Windows: Building libraries"
  _cd "$PLTHOME"
  win_build_step RKT "compiler" -N raco -l- raco setup -Dl compiler

  win_build_step VSNET3M "mzcom"

  _cd "$PLTHOME"; win_build_step RKT "raco setup" $SETUP_ARGS

  header -s "Windows: Building Cygwin libraries"
  _mcd "$PLTHOME/src/build"
  _run ../configure --disable-gracket
  _cd "racket/dynsrc"
  show "Running \"make\" for Cygwin"
  make && make cygwin-install \
  || exit_error "Errors when running \"make\" for Cygwin"

  # Borland is no longer supported:
  # header -s "Windows: Building Borland libraries"
  # _cd "$PLTHOME/src/racket/dynsrc"
  # _run bcc32 -I"../include" -I"g:/borland/bcc55/include" \
  #        -o"mzdynb.obj" -c "mzdyn.c"
  # _md "$PLTHOME/lib/bcc"
  # _cp "mzdynb.obj" "mzdynb.def" "$PLTHOME/lib/bcc"

  _cd "$PLTHOME"
  win_build_step RKT "winvers" -l setup/winvers; sleep 240

}

## ============================================================================

BUILD_DOCS_AND_PDFS() {

  header -s "Copying and making \"$docdir\""

  _rmcd "$maindir/$docdir"
  html_begin "Documentation"
  html_table_begin
  {
    html_file_row "html" \
      "html files for on-line browsing (same as racket/collecs/doc)"
    _rm "html"
    _cp -r "$workdir/$installdir/doc" "html"
  }
  if is_yes make_pdf_docs; then
    html_file_row "pdf" "pdf versions of the manuals"
    _rmcd "pdf"
    # avoid any work except for the pdf generation
    _run "$PLTHOME/bin/racket" $SETUP_ARGS \
           --no-zo --no-launcher --no-install --no-post-install \
           --no-info-domain --no-docs --no-planet \
           --doc-pdf .
    _cd ..
  else
    show "Skipping pdf build"
  fi
  html_table_end
  html_end

}

## ============================================================================

COPY_AND_BUILD_BINARY_DIRS() {

  ## --------------------------------------------------------------------------
  # This creates build-related directories.  The installers and
  # pre-installers are built in their own steps.

  ## --------------------------------------------------------------------------
  header -s "Copying and making \"$bindir\""

  _rmcd "$maindir/$bindir"

  html_begin "Binaries"
  html_show "Note that the binaries include the CGC versions."
  html_table_begin

  local m
  for m in "${machines[@]}"; do
    machine="$m"
    machineget mplatform=platform
    mplatformname="$(name_of_platform "$mplatform")"
    html_file_row "$mplatform" "Binaries for $mplatformname"
    {
      _rmcd "$mplatform"
      local btgz="$installdir-$mplatform-binaries.tgz"
      local ftgz="$installdir-$mplatform-full.tgz"
      local prfx=""
      if [[ "$m" != "$workmachine" ]]; then prfx="${m}:"; fi
      _scp "${prfx}$fulltgz" "$ftgz"
      local extratext="$(extra_description_of_platform "$mplatform")"
      html_begin "$mplatformname binaries ($mplatform)"
      html_show "These are the $mplatformname binary files." $extratext
      html_table_begin
      # The following two things do not exist until the bundle script runs
      html_file_row "$installdir" "The binary files part of the build tree"
      html_file_row "$btgz" "An archive of the above"
      html_file_row "$ftgz" "An archive of the fully-built tree" \
        "<br><small>(without the \"src\" tree)</small>"
      html_table_end
      html_end
      _cd ..
    }
  done

  html_table_end
  html_end

  ## --------------------------------------------------------------------------
  header "Making \"$stampfile\""

  _cd "$maindir"
  _rm "$stampfile"
  echo "$timestamp $version" > "$stampfile"

}

## ============================================================================

BUILD_BUNDLES() {

  ## --------------------------------------------------------------------------
  # the index in this directory is made by BUILD_INSTALLERS below

  header -s "Creating pre-installer bundles"
  _rmd "$maindir/$preinstdir"
  show "Running the bundle script"
  local bundleflags=""
  if [[ "$releasing" = "yes" ]]; then bundleflags="$bundleflags ++release"; fi
  _run "$PLTHOME/bin/racket" \
         "$PLTHOME/$bundlescript" -o "$maindir/$preinstdir" $bundleflags

}

## ============================================================================

# platform-specific installer makers:
# $1 is input file, $2 is the output (without suffix)
# $3 is the package name (textual/racket), $4 is the type (bin/src)
# $5 is the platform name (unix/mac/win for src distributions)

#----------------------------------------
tgz_to_tgz() {
  local srctgz="$1" tgt="$2" pname="$3" ptype="$4" srcplatform="$5"; shift 5
  local distname="$(name_of_dist_package "$pname" | tr ' A-Z' '-a-z')"
  local savedpwd="$(pwd)"
  local srcdir="$distname-$version"
  _rmcd "$tmpdir/tgz-to-tgz-$$"
  _tgunzip "$srctgz"
  _mv "$installdir" "$srcdir"
  _tgzip "$tgt.tgz" "$srcdir"
  _cd "$savedpwd"
  _rm "$tmpdir/tgz-to-tgz-$$"
}
#----------------------------------------
tgz_to_sh() {
  local srctgz="$1" tgt="$2" pname="$3" ptype="$4" srcplatform="$5"; shift 5
  local distname="$(name_of_dist_package "$pname" | tr ' A-Z' '-a-z')"
  local tmppackdir="$tmpdir/pack-$$"
  local tmptgz="$tmpdir/pack-$$.tar.gz"
  local treesize installerlines archivecksum
  # check paths data in configure script
  if [[ "$unixpathcheckscript" != "DONE" ]]; then
    show "Checking paths in configure script"
    _run "$PLTHOME/$unixpathcheckscript"
    unixpathcheckscript="DONE"
  fi
  savedpwd="$(pwd)"
  _rmcd "$tmppackdir"
  _tgunzip "$srctgz"
  _run chmod -R g+w "$tmppackdir"
  _cd "$installdir"
  _run fakeroot -- pax -w -z -f "$tmptgz" *
  treesize="$(get_first du -hs .)"
  _cd "$savedpwd"
  _rm "$tmppackdir"
  archivecksum="$(get_first cksum "$tmptgz")"
  local humanname="$(name_of_dist_package "$pname") v$version"
  local tgtname="$distname"
  if [[ "$releasing" != "yes" ]]; then tgtname="$tgtname-$version"; fi
  echo "Writing \"$tgt.sh\""
  { echo "#!/bin/sh"
    echo ""
    echo "# This is a self-extracting shell script for $humanname."
    echo "# To use it, just run it, or run \"sh\" with it as an argument."
    echo ""
    echo "DISTNAME=\"$humanname\""
    echo "PNAME=\"$distname\""
    echo "TARGET=\"$tgtname\""
    echo "BINSUM=\"$archivecksum\""
    echo "ORIGSIZE=\"$treesize\""
    echo "RELEASED=\"$releasing\""
  } > "$tgt.sh" \
  || exit_error "Could not write \"$tgt.sh\""
  installerlines=$(( $(get_first wc -l "$PLTHOME/$unixinstallerscript") +
                     $(get_first wc -l "$tgt.sh") +
                     2 ))
  echo "BINSTARTLINE=\"$installerlines\"" >> "$tgt.sh"
  cat "$PLTHOME/$unixinstallerscript" >> "$tgt.sh"
  cat "$tmptgz" >> "$tgt.sh"
  chmod +x "$tgt.sh"
  rm "$tmptgz"
}
#----------------------------------------
tgz_to_zip() {
  local srctgz="$1" tgt="$2" pname="$3" ptype="$4" srcplatform="$5"; shift 5
  local distname="$(name_of_dist_package "$pname" | tr ' ' '-')"
  local savedpwd="$(pwd)"
  local srcdir="$installdir"
  _rmcd "$tmpdir/tgz-to-zip-$$"
  _tgunzip "$srctgz"
  if [[ "$ptype" = "src" ]]; then
    srcdir="$distname-$version"
    _mv "$installdir" "$srcdir"
  fi
  _zip "$tgt.zip" "$srcdir"
  _cd "$savedpwd"
  _rm "$tmpdir/tgz-to-zip-$$"
}
#----------------------------------------
make_dmg() { # inputs: dir, dmg, internet-enabled?
  local srcdir="$1" tgtdmg="$2" internet_enabled="$3"; shift 3
  local tmpdmg="${tgtdmg%.dmg}-tmp.dmg"
  local src="$(basename "$srcdir")"
  local myself="$(id -nu):$(id -ng)"
  show "Making \"$tgtdmg\" from \"$srcdir\""
  _cd "$(dirname "$srcdir")"
  _run sudo rm -f "$tgtdmg" "$tmpdmg"
  # It should be possible to create dmgs normally, but they'd be created with
  # the same user id of whoever runs this script...
  _run sudo chown -R root:admin "$src"
  # The following command should work fine, but it looks like hdiutil in 10.4
  # is miscalculating the needed size, making it too big in our case (and too
  # small with >8GB images).  It seems that it works to first generate an
  # uncompressed image and then convert it to a compressed one.
  # _run sudo hdiutil create -format UDZO -imagekey zlib-level=9 -ov \
  #                      -mode 555 -volname "$src" -srcfolder "$src" "$tgtdmg"
  # so: [1] create an uncompressed image
  _run sudo hdiutil create -format UDRW -ov \
                       -mode 555 -volname "$src" -srcfolder "$src" "$tmpdmg"
  # [2] remove the source tree
  _run sudo rm -rf "$src"
  # [3] create the compressed image from the uncompressed image
  _run sudo hdiutil convert -format UDZO -imagekey zlib-level=9 -ov \
                       "$tmpdmg" -o "$tgtdmg"
  # [4] remove the uncompressed image
  _run sudo chown "$myself" "$tgtdmg" "$tmpdmg"
  _rm "$tmpdmg"
  # this will make browsers get the dmg, mount, copy contents, unmount
  if [[ "$internet_enabled" = "yes" ]]; then
    _run hdiutil internet-enable "$tgtdmg"
  fi
}
#----------------------------------------
do_tgz_to_dmg() {
  local internet_enabled="$1" tmptgz="$2" tmpdmg="$3" version="$4"
  local pname="$5" ptype="$6" srcplatform="$7"
  shift 7
  local distname="$(name_of_dist_package "$pname")"
  distname="$distname v$version"
  if [[ "$ptype" != "bin" ]]; then
    distname="$distname $(name_of_dist_type "$ptype")"
  fi
  local savedpwd="$(pwd)"
  _rm "$tmpdmg"
  _rmcd "$tmpdir/tgz-to-dmg-$$"
  _mcd "$distname"
  _tgunzip "$tmptgz"
  _rm "$tmptgz"
  _mv "$installdir" "$distname"
  _cd "$tmpdir/tgz-to-dmg-$$"
  make_dmg "$distname" "$tmpdmg" "$internet_enabled"
  _cd "$savedpwd"
  _rm "$tmpdir/tgz-to-dmg-$$"
}
tgz_to_some_dmg() {
  local internet_enabled="$1"; shift 1
  local srctgz="$1" tgt="$2" pname="$3" ptype="$4" srcplatform="$5"; shift 5
  local tmptgz="$tmpdir/tgz2dmg.tgz"
  local tmpdmg="$tmpdir/tgz2dmg.dmg"
  _scp "$srctgz" "${dmgmachine}:$tmptgz"
  run_part "$dmgmachine" "do_tgz_to_dmg" "$internet_enabled" \
             "$tmptgz" "$tmpdmg" "$version" "$pname" "$ptype" "$srcplatform"
  _scp "${dmgmachine}:$tmpdmg" "$tgt.dmg"
}
tgz_to_dmg() {
  tgz_to_some_dmg "no" "$@"
}
tgz_to_idmg() { # same as ..._dmg, but makes it internet-enabled
  tgz_to_some_dmg "yes" "$@"
}
#----------------------------------------
do_tgz_to_exe() {
  local tmptgz="$1" tmpexe="$2" nsistgz="$3"
  local pname="$4" ptype="$5" srcplatform="$6"
  shift 6
  local savedpwd="$(pwd)"
  _rmcd "$tmpdir/tgz-to-exe-$$"
  _tgunzip "$nsistgz"
  _tgunzip "$tmptgz"
  show "Running NSIS to create the installer"
  "/c/Program Files/NSIS/makensis.exe" /V3 "installer.nsi" | tr -d '\r' \
  || exit_error "NSIS build failed"
  _mv "installer.exe" "$tmpexe"
  _cd "$savedpwd"
  sleep 1 # looks like there are some problems deleting this immediately
  _rm "$tmpdir/tgz-to-exe-$$"
}
tgz_to_exe() {
  local srctgz="$1" tgt="$2" pname="$3" ptype="$4" srcplatform="$5"; shift 5
  local nsistgz="$tmpdir/racket-nsis.tgz"
  local tmptgz="$tmpdir/tgz2exe.tgz"
  local tmpexe="$tmpdir/tgz2exe.exe"
  _rm "$tmpdir/racket-nsis-$$"
  _cp -r "$PLTHOME/$nsisdir" "$tmpdir/racket-nsis-$$"
  _cd "$tmpdir/racket-nsis-$$"
  show "Writing \"racket-defs.nsh\""
  { local def='!define'
    local distname="$(name_of_dist_package "$pname")"
    local winplatform="${srcplatform%-win32}"
    echo "$def RKTVersion \"$version\""
    # this must be four numbers
    echo "$def RKTVersionLong \"$version1.$version2.$version3.$version4\""
    # Full name for the package, and a short name for installer texts
    echo "$def RKTHumanName \"$distname v$version ($winplatform)\""
    echo "$def RKTShortName \"$distname\""
    if [[ "$releasing" != "yes" ]]; then
      echo "$def RKTStartName \"$distname v$version\""
    else
      echo "$def RKTStartName \"$distname\""
    fi
    distname="$(echo "$distname" | tr ' ' '-')"
    if [[ "$releasing" != "yes" ]]; then
      echo "$def RKTDirName \"$distname-$version\""
    else
      echo "$def RKTDirName \"$distname\""
    fi
    echo "$def RKTRegName \"$distname-$winplatform-$version\""
    if [[ "$winplatform" = "x86_64" ]]; then
      echo "$def RKTProgFiles \"\$PROGRAMFILES64\""
    else
      echo "$def RKTProgFiles \"\$PROGRAMFILES\""
    fi
    if [[ "$pname" = "mz" ]]; then echo "$def SimpleInstaller"; fi
  } > "racket-defs.nsh" \
    || exit_error "Could not write \"racket-defs.h\""
  local line="---------- racket-defs.nsh ----------"
  echo "$line"
  cat "racket-defs.nsh"
  echo "$line" | sed 's/./-/g'
  _tgzip "$nsistgz" *
  _cd "$tmpdir"
  _rm "$tmpdir/racket-nsis-$$"
  _scp "$nsistgz" "${nsismachine}:$nsistgz"
  _scp "$srctgz" "${nsismachine}:$tmptgz"
  run_part "$nsismachine" \
    "do_tgz_to_exe" "$tmptgz" "$tmpexe" "$nsistgz" \
                    "$pname" "$ptype" "$srcplatform"
  _scp "${nsismachine}:$tmpexe" "$tgt.exe"
}
#----------------------------------------

do_installers_page_body() { # input: selector-html table-html
  local selector="$1" table="$2"; shift 2
  local dtype dtypename dists dist platforms ptype ptypename
  local d file fsize idx expl
  local dists="plt mz full"
  local dtypes="bin src"
  { echo "<script type=\"text/javascript\" language=\"JavaScript\"><!--"
    echo "  var download_target = '';"
    echo "  function dnld_update() {"
    echo "    var dn = document.dnld;"
    echo "    var d = dn.dist.value+'-$version-'+dn.ty_pl.value;"
    echo "    var c;"
    echo "    if (false) c = 'bogus';"
    for file in *-"$version"-*; do
      local base="$(echo "$file" | sed -e 's/\.[a-z].*$//')"
      expl="$(echo "$base" | cut -d - -f 3-)"
      expl="$(installer_of_dist_type_platform "$expl")"
      expl="$(explanation_of_installer_type "$expl")"
      fsize="$(get_first du -h "$file")"
      echo "    else if (d == '$base')" \
                  "{ t = '$file'; c = '$file ($fsize)\n$expl'; }"
    done
    if [[ ! -e "../$cleantgz" ]]; then
      show "Warning: no \"../$cleantgz\" file for full-...-src at $(pwd)" 1>&2
    else
      # another case that matches full-...-src and uses the clean tgz
      file="../$cleantgz"
      fsize="$(get_first du -h "$file")"
      expl="This is a gzipped-tarball of the full Racket sources,"
      expl="$expl for all platforms."
      echo "    else if (/^full-.*-src-*/.test(d))" \
                  "{ t = '$file'; c = '$file ($fsize)\n$expl' }"
    fi
    echo "    else { t = ''; c = ''; }"
    echo "    download_target = t;"
    echo "    if (c == '') dn.comment.value =" \
                "'Invalid selection (no such configuration)';"
    echo "    else dn.comment.value =" \
                "'You are about to download: '+c;"
    echo "  }"
    echo "function do_download() {"
    echo "  if (download_target == '') window.alert('Invalid configuration');"
    echo "  else window.location = download_target;"
    echo "}"
    echo "// --></script>"
    echo "<table align=\"center\" border=\"3\" rules=\"rows\"" \
         "bordercolor=\"#ff9600\" bgcolor=\"#ffe5c0\"><tr><td>"
    echo "<form name=\"dnld\">"
    echo "Distribution: <select name=\"dist\" onChange=\"dnld_update()\">"
    for d in $dists; do
      echo "  <option value=\"$d\">$(name_of_dist_package "$d")</option>"
    done
    echo "</select>&nbsp;&nbsp;"
    echo "Platform+Type: <select name=\"ty_pl\" onChange=\"dnld_update()\">"
    for dtype in $dtypes; do
      for d in $(platforms_of_dist_type "$dtype"); do
        if ls *-"$version"-* | grep -q -- "-$d[.]"; then
          local option="<option value=\"$dtype-$d\">$(name_of_platform "$d")"
          if [[ "$dtype" != "bin" ]]; then
            option="$option ($(name_of_dist_type "$dtype"))"
          fi
          option="$option</option>"
          echo "  $option"
        fi
      done
    done
    echo "</select>&nbsp;&nbsp;"
    echo "<input type=\"button\" value=\"Download\" OnClick=\"do_download()\">"
    echo "<br>"
    echo "<textarea name=\"comment\" cols=\"80\" rows=\"3\"" \
         "wrap=\"soft\" readonly></textarea>"
    echo "</form></td></tr></table>"
    echo "<script type=\"text/javascript\" language=\"JavaScript\"><!--"
    echo "  dnld_update();"
    echo "// --></script>"
    echo "<div style=\"text-align: right; font-size: small;\">"
    echo "  <a href=\"table.html\">[static table]</a>"
    echo "</div>"
  } >> "$selector"
  local NAcell="<td><font color=\"#808080\">N/A</font></td>"
  local SRCcell="<td><a href=\"../$cleantgz\"><tt>$cleantgz</tt><a></td>"
  idx=0
  for dtype in $dtypes; do
    dtypename="$(name_of_dist_type "$dtype")" || exit_error "Bad dist type"
    echo "<tr bgcolor=\"#ffff80\"><td align=\"center\">" >> "$table"
    echo "<b>$dtypename distributions</b></td>" >> "$table"
    for d in $dists; do
      echo "<td><b>$(name_of_dist_package "$d")</b></td>" >> "$table"
    done
    echo "</tr>" >> "$table"
    for ptype in $(platforms_of_dist_type "$dtype"); do
      if [[ "$dtype" != "bin" || -d "$maindir/$bindir/$ptype" ]]; then
        ptypename="$(name_of_platform "$ptype")" \
        || exit_error "Bad dist package"
        echo "<tr><td align=\"center\" bgcolor=\"#ffffa0\">" >> "$table"
        echo "<b>$ptypename</b></td>" >> "$table"
        for d in $dists; do
          if [[ "$dtype" = "src" ]]; then case "$d" in
            ( "plt" | "mz" ) ;;
            ( "full" ) echo "$SRCcell" >> "$table"; continue ;;
            ( * ) echo "$NAcell" >> "$table"; continue ;;
          esac; fi
          distributions2[idx++]="$d-$dtype-$ptype"
          file="$(ls "$d-$version-$dtype-$ptype."*)"
          if [[ "$file" = "" ]]; then
            echo "<td>(missing)</td>" >> "$table"
          else
            local fsize="$(get_first du -h "$file")"
            echo "<td><a href=\"$file\"><tt>$file</tt></a>" >> "$table"
            echo "<small>($fsize)</small></td>" >> "$table"
          fi
        done
        echo "<tr>" >> "$table"
      fi
    done
  done
}

BUILD_INSTALLERS() {

  ## --------------------------------------------------------------------------
  header -s "Creating platform-specific installers"
  _rmd "$maindir/$instdir"
  _cd "$maindir/$preinstdir"
  html_begin "Pre-installers"
  html_table_begin
  local tgz idx
  idx=0
  for tgz in *.tgz; do
    local dname="$(basename "$tgz" .tgz)"
    distributions1[idx++]="$dname"
    local dpackage="$( echo "$dname" | cut -d - -f 1)"
    local dtype="$(    echo "$dname" | cut -d - -f 2)"
    local dplatform="$(echo "$dname" | cut -d - -f 3-)"
    html_file_row "$tgz" "$(name_of_dist_type "$dtype") distribution of" \
      "$(name_of_dist_package "$dpackage") for" \
      "$(name_of_platform "$dplatform")"
    convert="tgz_to_$(installer_of_dist_type_platform "$dtype-$dplatform")"
    header +s "Making \"$dpackage-$dtype\" installer for \"$dplatform\""
    show "Using \"$convert\" to convert \"$dname\""
    "$convert" "$maindir/$preinstdir/$tgz" \
               "$maindir/$instdir/$dpackage-$version-$dtype-$dplatform" \
               "$dpackage" "$dtype" "$dplatform"
    _cd "$maindir/$preinstdir"
  done
  html_table_end
  html_end

  _cd "$maindir/$instdir"
  show "Making the distributions page"
  _rm "$tmpdir/tmp-selector" "$tmpdir/tmp-table"
  do_installers_page_body "$tmpdir/tmp-selector" "$tmpdir/tmp-table"
  # selector page
  html_begin "Installers"
  html_show -f "$tmpdir/tmp-selector"
  html_end
  # static table page
  html_begin "Installers (static)" "table.html"
  html_table_begin "all"
  html_show -f "$tmpdir/tmp-table"
  _rm "$tmpdir/tmp-selector" "$tmpdir/tmp-table"
  html_table_end
  html_end

  local f sorted1 sorted2
  show "Checking generated pre-distribution and distributions on index page"
  sorted1="$(for f in "${distributions1[@]}"; do echo "$f"; done | sort)"
  sorted2="$(for f in "${distributions2[@]}"; do echo "$f"; done | sort)"
  if [[ "$sorted1" = "$sorted2" ]]; then
    show "File lists identical, good."
  else
    show "File lists do not match."
    show "Generated pre-distributions:"
    echo "$sorted1"
    show "Indexed distributions:"
    echo "$sorted2"
    exit_error "Fix this script"
  fi

}

## ============================================================================

move_from_maindir() { # input: file-name
  if [[ -e "$maindir/$1" ]]; then
    if [[ -e "$1" ]]; then _rmd "TEMP_WEB"; _mv "$1" "TEMP_WEB"; fi
    _mv "$maindir/$1" .
    _rm "TEMP_WEB"
  elif [[ ! -e "$1" ]]; then exit_error "\"$1\" is not in $maindir or $(pwd)"
  else show "Skipping \"$1\""
  fi
}
copy_from() { # input: directory file-name
  _rmcd "TEMP_WEB"
  show "Copying: \"$1/$2\" to \"$(pwd)\""
  ( cd "$1" ; tar cf - "$2" ) | tar xf - \
  || exit_error "Could not copy \"$1/$2\" to \"$(pwd)\""
  _cd ".."
  if [[ -e "$2" ]]; then _mv "$2" "TEMP_WEB/TEMP_WEB"; fi
  _mv "TEMP_WEB/$2" .
  _rm "TEMP_WEB"
}

BUILD_WEB() {

  # prewebdir is set to the directory where the pre pages go (used by
  # BUILD_PRE_WEB below)
  # cases for "hidden" results: building a major version, or a non-default path
  prewebdir="$prewebdir_default"
  if [[ "$reallyreleasing" = "yes" ]]; then prewebdir="$prewebdir/$version"
  elif [[ "$gitbranch" != "master" ]]; then prewebdir="$prewebdir/$gitbranch"
  elif [[ "$releasing" = "yes" ]];     then prewebdir="$prewebdir/$version"
  fi

  # should always set prewebdir above, the rest is optional
  if ! is_yes make_web; then return; fi

  ## --------------------------------------------------------------------------
  _rmcd "$maindir/$webdir"
  local webflags=""
  webflags="$webflags -w -f -o $maindir/$webdir"
  webflags="$webflags -e $maindir/$internaldir/web/all.rkt"
  # distribute only if this is a normal build
  if [[ "$prewebdir" = "$prewebdir_default" ]]; then
    header -s "Building and distributing web content"
    webflags="$webflags --dist"
  else
    header -s "Building web content -- not distributing"
  fi
  GIT_DIR="$maindir/$cleandir/.git" \
  KNOWN_MIRRORS_FILE="$maindir/$knownmirrors" \
    _run "$PLTHOME/$webscript" $webflags

}

BUILD_PRE_WEB() {

  ## --------------------------------------------------------------------------
  if [[ "$prewebdir" = "$prewebdir_default" ]]; then
    header -s "Making build web pages"
  else
    header -s "Making build web pages at $prewebdir"
  fi

  _mcd "$maindir/$prewebdir"

  html_begin "Racket Nightly Builds"
  html_table_begin
  #----
  move_from_maindir "$installersdir"
  html_file_row "$installersdir" "Installers" \
    "<br><small>(these are platform-specific distribution files, similar to" \
    "standard distributions.)</small>"
  #----
  move_from_maindir "$docdir"
  html_file_row "$docdir" "Documentation files"
  #----
  move_from_maindir "$bindir"
  html_file_row "$bindir" "Platform-specific binary files"
  #----
  move_from_maindir "$preinstdir"
  html_file_row "$preinstdir" "Pre-installer files" \
    "<br><small>(these contain distribution files in tgz format, which are" \
    "used to create platform-specific installers)</small>"
  #----
  copy_from "$workdir" "$installdir"
  html_file_row "$installdir" "A complete build tree (built on $platform)"
  #----
  move_from_maindir "$cleantgz"
  html_file_row "$cleantgz" \
    "The complete repository source tree packed in a gzipped tarball"
  #----
  html_file_row "script.html" "Sample scripts for using nightly builds" \
    "<br><small>(read this if you want to use automatic scripts to keep" \
    "an up-to-date installation)</small>"
  #----
  move_from_maindir "$stampfile"
  html_file_row "$stampfile" "Timestamp+version file" \
    "<br><small>(updated only after a successful build, useful for" \
    "automatic scripts)</small>"
  #----
  # don't copy this, since it's still being written to
  _rm "$buildlogfile"; ln "$maindir/$buildlogfile" "$buildlogfile"
  html_file_row "$buildlogfile" "Full build log"
  #----
  html_table_end
  html_end

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

  local F F2

  # copy resources built by the web script for the pre pages
  _cd "$maindir/$webdir/$pretemplatedir"
  find . -type f -printf '%P\n' | \
  while read F; do
    F2="$maindir/$prewebdir/$F"
    if ! [[ "$F" -nt "$F2" ]]; then continue; fi
    # skip files that will be patched below
    if [[ -e "$F2.title." ]]; then continue; fi
    _md "$maindir/$prewebdir/$(dirname "$F")"
    _cp "$F" "$maindir/$prewebdir/$F"
  done

  # patch generated html files with their templates (or the generic one)
  _cd "$maindir/$prewebdir"
  find . -type f -name "*.title." -printf '%P\n' | \
  while read F; do
    F="${F%.title.}"
    if [[ ! -e "$F" ]]; then continue; fi
    show "Patching $F"
    mv "$F" "$F.temp."
    local htmltemplate="$maindir/$webdir/$pretemplatedir/$F"
    if [[ ! -e "$htmltemplate" ]]; then
      htmltemplate="$maindir/$webdir/$pretemplatedir/template.html"
    fi
    { if grep -q "TITLE" "$htmltemplate"; then
        awk '/^{{{TITLE}}}$/{p=0};p;BEGIN{p=1}' "$htmltemplate"
        cat "$F.title."
        awk '/^{{{CONTENT}}}$/{p=0};p;/^{{{TITLE}}}$/{p=1}' "$htmltemplate"
      else
        awk '/^{{{CONTENT}}}$/{p=0};p;BEGIN{p=1}' "$htmltemplate"
      fi
      cat "$F.temp."
      awk '/^{{{VERSION}}}$/{p=0};p;/^{{{CONTENT}}}$/{p=1}' "$htmltemplate"
      echo "(version $version, $htmltimestamp)"
      awk 'END{p=0};p;/^{{{VERSION}}}$/{p=1}' "$htmltemplate"
    } > "$F"
    rm "$F.title." "$F.temp."
  done

  ## --------------------------------------------------------------------------
  if [[ "$prewebdir" = "$prewebdir_default" ]]; then
    header "Creating a site-map"
    _cd "$maindir/$prewebdir"
    _run "$PLTHOME/$sitemapdir/sitemap_gen.py" \
         --config="$PLTHOME/$sitemapdir/plt-pre.xml" \
           > /dev/null
  fi

  ## --------------------------------------------------------------------------
  # Make the status script available for use

  if [[ "$prewebdir" = "$prewebdir_default" ]]; then
    _cd "$maindir/$prewebdir"
    local target="$maindir/$prewebdir_default/$(basename "$statusscript")"
    {
      echo "#!/bin/bash"
      echo ""
      local var
      for var in tmpdir lockfile statusfile statusfile_last \
                 buildlogfile bglogfile; do
        local val; eval val="\$$var"
        echo "$var=\"$val\""
      done
      echo ""
      cat "$PLTHOME/$statusscript"
    } > "$target"
    chmod +x "$target"
  fi

}


###############################################################################
### Main dispatch

if [[ "$1" = "--dispatch" ]]; then
  shift
  while [[ "$1" = *"="* ]]; do eval "export $1"; shift; done
  machine="$1"; go="$2"; shift 2
  init_repo_vars # set the repository variables according to the env vars
  show "Working on $machine, dispatching to $go($*)"
  "$go" "$@"
  if [[ "x$platform" = "x" ]]; then show "Done working on $machine"
  else show "Done working on $machine($platform)"; fi
elif [[ "$buildlog" = "yes" ]]; then
  show "Working on $machine"
  rm -f "$maindir/$buildlogfile"
  { echo "This is the build log, generated by $buildscript"; echo ""
    echo "Search for \"BOOM\" for any errors."; echo ""
    # set | grep "^[a-z].*=" | awk '{ print "  " $0 }'; echo ""
  } > "$maindir/$buildlogfile"
  chcon --type=httpd_sys_content_t "$maindir/$buildlogfile"
  _rm "$curlog_prefix-$buildlogfile"
  _ln "$maindir/$buildlogfile" "$curlog_prefix-$buildlogfile"
  if [[ "$buildlog" = "only" ]]; then
    exec >> "$maindir/$buildlogfile" 2>&1
    MAIN "$@"
  else
    MAIN "$@" 2>&1 | tee -a "$maindir/$buildlogfile"
  fi
else
  show "Working on $machine"
  MAIN "$@"
fi

exit

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