#!/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="dublin"
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"
# DrRacket 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"
dmgdir="$scriptdir/dmg"
dmgbackground="$dmgdir/racket-rising.png"
dmgscriptname="$HOME/work/dmgscriptname" # absolute
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 "sdmg" ;;
    ( "src-win"         ) echo "zip"  ;;
    ( "bin-"*"-linux"*  ) echo "sh"   ;;
    ( "bin-"*"-freebsd" ) echo "sh"   ;;
    ( "bin-"*"-solaris" ) echo "sh"   ;;
    ( "bin-"*"-darwin"  ) echo "sh"   ;;
    ( "bin-"*"-osx-mac" ) echo "dmg"  ;;
    ( "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 -\"." ;;
    ( "sdmg" ) echo "Mount this disk image and copy the Racket source folder" \
                    "to your disk." ;;
    ( "dmg" )  echo "Mount this disk image and copy the Racket folder to" \
                    "the Applications folder on your disk." ;;
    ( "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 (math)" ../src/get-libs.rkt math
  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, bg-image
  local srcdir="$1" tgtdmg="$2" tmpbg="$3"; shift 3
  local tmpdmg="${tgtdmg%.dmg}-tmp.dmg"
  local src="$(basename "$srcdir")"
  local myself="$(id -nu):$(id -ng)"
  show "Making \"$tgtdmg\" from \"$srcdir\""
  if [[ "x$tmpbg" != "x-" ]]; then _cp "$tmpbg" "$srcdir"; fi
  _cd "$(dirname "$srcdir")"
  _rm "$tgtdmg" "$tmpdmg"
  # 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 hdiutil create -format UDRW -ov \
                      -mode 755 -volname "$src" -srcfolder "$src" "$tmpdmg"
  # [2] remove the source tree
  _rm "$src"
  # [3] do the expected dmg layout (see below)
  if [[ "x$tmpbg" != "x-" ]]; then
    easy_dmg_layout "$tmpdmg" "$src" "$(basename "$tmpbg")"
  fi
  # [4] create the compressed image from the uncompressed image
  _run hdiutil convert -format UDBZ -imagekey zlib-level=9 -ov \
                       "$tmpdmg" -o "$tgtdmg"
  # [5] remove the uncompressed image
  _rm "$tmpdmg"
}
easy_dmg_layout() {
  local tmpdmg="$1" volname="$2" bg="$3"; shift 3
  show "Mounting image for layout"
  local mnt="mounted-dmg-$$"
  _run hdiutil attach -readwrite -noverify -noautoopen \
                      -mountpoint "$tmpdir/$mnt" "$tmpdmg"
  show "Creating layout via Finder"
  # see also https://github.com/andreyvit/yoursway-create-dmg
  /usr/bin/osascript <<-EOF
	tell application "Finder"
	  -- look for a single disk with the mount point as its name
	  -- (maybe this works only on newer osx versions?)
	  set myDisks to every disk of desktop
	  set theDMGDisk to ""
	  repeat with d in myDisks
	    if name of d = "$mnt"
	      if theDMGDisk = ""
	        set theDMGDisk to d
	      else
	        error "Too many attached DMGs found!"
	      end if
	    end if
	  end repeat
	  if theDMGDisk = "" then error "Attached DMG not found!"
	  -- found a single matching disk, continue
	  tell theDMGDisk
	    open
	    set current view of container window to icon view
	    set toolbar visible of container window to false
	    set statusbar visible of container window to false
	    set bounds of container window to {320, 160, 1000, 540}
	    set theViewOptions to the icon view options of container window
	    set arrangement of theViewOptions to not arranged
	    set icon size of theViewOptions to 128
	    set text size of theViewOptions to 16
	    set background picture of theViewOptions to file "$bg"
	    make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"}
	    set position of item "$volname" of container window to {170, 180}
	    set position of item "$bg" of container window to {900, 180}
	    set position of item "Applications" of container window to {500, 180}
	    set name of file "$bg" to ".$bg"
	    close
	    open
	    update without registering applications
	    delay 5
	    close
	  end tell
	end tell
EOF
  sync; sync
  _run sudo chown -R root:admin "$tmpdir/$mnt/"* "$tmpdir/$mnt/".[^.]*
  sync; sync
  _run hdiutil detach "$tmpdir/$mnt"
}
#----------------------------------------
do_tgz_to_dmg() {
  local tmptgz="$1" tmpdmg="$2" tmpbg="$3" script="$4"
  local version="$5" pname="$6" ptype="$7" srcplatform="$8"
  shift 8
  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)"
  unset LD_LIBRARY_PATH # no warnings (http://openradar.appspot.com/11894054)
  _rm "$tmpdmg"
  _rmcd "$tmpdir/tgz-to-dmg-$$"
  _mcd "$distname"
  _tgunzip "$tmptgz"
  _rm "$tmptgz"
  _mv "$installdir" "$distname"
  _cd "$distname"
  if [[ "x$script" != "x-" && -e "$script" ]]; then "$script"; fi
  _cd "$tmpdir/tgz-to-dmg-$$"
  make_dmg "$distname" "$tmpdmg" "$tmpbg"
  _cd "$savedpwd"
  _rm "$tmpdir/tgz-to-dmg-$$"
}
tgz_to_dmg() {
  local simpledmg="no"
  if [[ "x$1" = "x--simpledmg" ]]; then shift; simpledmg="yes"; fi
  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"
  local tmpbg="-"
  if [[ "$simpledmg" = "no" ]]; then
    tmpbg="$tmpdir/bg.png"
    _scp "$PLTHOME/$dmgbackground" "${dmgmachine}:$tmpbg"
  fi
  local script="-"
  if [[ "$simpledmg" = "no" && -e "$dmgscriptname" ]]; then
    script="$(cat "$dmgscriptname")"
  fi
  run_part "$dmgmachine" "do_tgz_to_dmg" \
             "$tmptgz" "$tmpdmg" "$tmpbg" "$script" \
             "$version" "$pname" "$ptype" "$srcplatform"
  _scp "${dmgmachine}:$tmpdmg" "$tgt.dmg"
}
tgz_to_sdmg() {
  tgz_to_dmg --simpledmg "$@"
}
#----------------------------------------
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

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