ruby-build

    #!/usr/bin/env bash
    # Copyright (c) 2011 Sam Stephenson
    #
    # Permission is hereby granted, free of charge, to any person obtaining
    # a copy of this software and associated documentation files (the
    # "Software"), to deal in the Software without restriction, including
    # without limitation the rights to use, copy, modify, merge, publish,
    # distribute, sublicense, and/or sell copies of the Software, and to
    # permit persons to whom the Software is furnished to do so, subject to
    # the following conditions:
    #
    # The above copyright notice and this permission notice shall be
    # included in all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
    # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    RUBY_BUILD_VERSION="20120216"

    set -E
    exec 3<&2 # preserve original stderr at fd 3

    resolve_link() {
      $(type -p greadlink readlink | head -1) "$1"
    }

    abs_dirname() {
      local cwd="$(pwd)"
      local path="$1"

      while [ -n "$path" ]; do
        cd "${path%/*}"
        local name="${path##*/}"
        path="$(resolve_link "$name" || true)"
      done

      pwd
      cd "$cwd"
    }

    build_failed() {
      { echo
        echo "BUILD FAILED"
        echo

        if ! rmdir "${TEMP_PATH}" 2>/dev/null; then
          echo "Inspect or clean up the working tree at ${TEMP_PATH}"

          if file_is_not_empty "$LOG_PATH"; then
            echo "Results logged to ${LOG_PATH}"
            echo
            echo "Last 10 log lines:"
            tail -n 10 "$LOG_PATH"
          fi
        fi
      } >&3
      exit 1
    }

    file_is_not_empty() {
      local filename="$1"
      local line_count="$(wc -l "$filename" 2>/dev/null || true)"

      if [ -n "$line_count" ]; then
        words=( $line_count )
        [ "${words[0]}" -gt 0 ]
      else
        return 1
      fi
    }

    install_package() {
      install_package_using "tarball" 1 $*
    }

    install_git() {
      install_package_using "git" 2 $*
    }

    install_package_using() {
      local package_type="$1"
      local package_type_nargs="$2"
      local package_name="$3"
      shift 3

      pushd "$TEMP_PATH" >&4
      "fetch_${package_type}" "$package_name" $*
      shift $(($package_type_nargs))
      make_package "$package_name" $*
      popd >&4

      echo "Installed ${package_name} to ${PREFIX_PATH}" >&2
    }

    make_package() {
      local package_name="$1"
      shift

      pushd "$package_name" >&4
      before_install_package "$package_name"
      build_package "$package_name" $*
      after_install_package "$package_name"
      fix_directory_permissions
      popd >&4
    }

    fetch_tarball() {
      local package_name="$1"
      local package_url="$2"

      echo "Downloading ${package_url}..." >&2
      { curl "$package_url" > "${package_name}.tar.gz"
        tar xzvf "${package_name}.tar.gz"
      } >&4 2>&1
    }

    fetch_git() {
      local package_name="$1"
      local git_url="$2"
      local git_ref="$3"

      echo "Cloning ${git_url}..." >&2
      { git clone --depth 1 --branch "$git_ref" "$git_url" "${package_name}"
      } >&4 2>&1
    }

    build_package() {
      local package_name="$1"
      shift

      if [ "$#" -eq 0 ]; then
        local commands="standard"
      else
        local commands="$*"
      fi

      echo "Installing ${package_name}..." >&2

      for command in $commands; do
        "build_package_${command}"
      done
    }

    build_package_standard() {
      local package_name="$1"

      if [ -z "${MAKEOPTS+defined}" ]; then
        MAKE_OPTS="$MAKEOPTS"
      elif [ -z "${MAKE_OPTS+defined}" ]; then
        MAKE_OPTS="-j 2"
      fi

      { ./configure --prefix="$PREFIX_PATH" $CONFIGURE_OPTS
        make $MAKE_OPTS
        make install
      } >&4 2>&1
    }

    build_package_autoconf() {
      { autoconf
      } >&4 2>&1
    }

    build_package_ruby() {
      local package_name="$1"

      { "$RUBY_BIN" setup.rb
      } >&4 2>&1
    }

    build_package_ree_installer() {
      local options=""
      if [[ "Darwin" = "$(uname)" ]]; then
        options="--no-tcmalloc"
      fi

      # Work around install_useful_libraries crash with --dont-install-useful-gems
      mkdir -p "$PREFIX_PATH/lib/ruby/gems/1.8/gems"

      { ./installer --auto "$PREFIX_PATH" --dont-install-useful-gems $options $CONFIGURE_OPTS
      } >&4 2>&1
    }

    build_package_rbx() {
      local package_name="$1"

      { ./configure --prefix="$PREFIX_PATH" --gemsdir="$PREFIX_PATH"
        rake install
      } >&4 2>&1
    }

    build_package_maglev() {
      build_package_copy

      { cd "${PREFIX_PATH}"
        ./install.sh
        cd "${PREFIX_PATH}/bin"
        echo "Creating symlink for ruby*"
        ln -fs maglev-ruby ruby
        echo "Creating symlink for irb*"
        ln -fs maglev-irb irb
      } >&4 2>&1
      echo
      echo "Run 'maglev start' to start up the stone before using 'ruby' or 'irb'"
    }

    build_package_jruby() {
      build_package_copy
      cd "${PREFIX_PATH}/bin"
      ln -fs jruby ruby
      install_jruby_launcher
      remove_windows_files
    }

    install_jruby_launcher() {
      cd "${PREFIX_PATH}/bin"
      { ./ruby gem install jruby-launcher
      } >&4 2>&1
    }

    remove_windows_files() {
      cd "$PREFIX_PATH"
      rm -f bin/*.exe bin/*.dll bin/*.bat bin/jruby.sh
    }

    build_package_copy() {
      mkdir -p "$PREFIX_PATH"
      cp -R . "$PREFIX_PATH"
    }

    before_install_package() {
      local stub=1
    }

    after_install_package() {
      local stub=1
    }

    fix_directory_permissions() {
      # Ensure installed directories are not world-writable to avoid Bundler warnings
      find "$PREFIX_PATH" -type d -exec chmod go-w {} \;
    }

    require_gcc() {
      local gcc="$(locate_gcc || true)"
      if [ -z "$gcc" ]; then
        { echo
          echo "ERROR: This package must be compiled with GCC, and we"
          echo "couldn't find a suitable \`gcc' binary on your system."
          echo "Please install GCC and try again."
          echo

          if [ "$(uname -s)" = "Darwin" ]; then
            echo "As of version 4.2, Xcode is LLVM-only and no longer"
            echo "includes GCC. You can install GCC with these binary"
            echo "packages on Mac OS X:"
            echo
            echo "https://github.com/kennethreitz/osx-gcc-installer/downloads"
            echo
          fi
        } >&3
        return 1
      fi

      export CC="$gcc"
    }

    locate_gcc() {
      local gcc gccs
      IFS=: gccs=($(gccs_in_path))

      verify_gcc "$CC" ||
      verify_gcc "$(command -v gcc || true)" || {
        for gcc in "${gccs[@]}"; do
          verify_gcc "$gcc" && break || true
        done
      }

      return 1
    }

    gccs_in_path() {
      local gcc path paths
      local gccs=()
      IFS=: paths=($PATH)

      shopt -s nullglob
      for path in "${paths[@]}"; do
        for gcc in "$path"/gcc-*; do
          gccs["${#gccs[@]}"]="$gcc"
        done
      done
      shopt -u nullglob

      printf :%s "${gccs[@]}"
    }

    verify_gcc() {
      local gcc="$1"
      if [ -z "$gcc" ]; then
        return 1
      fi

      local version="$("$gcc" --version || true)"
      if [ -z "$version" ]; then
        return 1
      fi

      if echo "$version" | grep LLVM >/dev/null; then
        return 1
      fi

      echo "$gcc"
    }

    version() {
      echo "ruby-build ${RUBY_BUILD_VERSION}"
    }

    usage() {
      { version
        echo "usage: ruby-build [-v|--verbose] definition prefix"
        echo "       ruby-build --definitions"
      } >&2

      if [ -z "$1" ]; then
        exit 1
      fi
    }

    list_definitions() {
      { for definition in "${RUBY_BUILD_ROOT}/share/ruby-build/"*; do
          echo "${definition##*/}"
        done
      } | sort
    }

    unset VERBOSE
    RUBY_BUILD_ROOT="$(abs_dirname "$0")/.."

    case "$1" in
    "-h" | "--help" )
      usage without_exiting
      { echo
        echo "  -v/--verbose     Verbose mode: print compilation status to stdout"
        echo "  --definitions    List all built-in definitions"
        echo
      } >&2
      exit 0
      ;;
    "--definitions" )
      list_definitions
      exit 0
      ;;
    "--version" )
      version
      exit 0
      ;;
    "-v" | "--verbose" )
      VERBOSE=true
      shift
      ;;
    esac

    DEFINITION_PATH="$1"
    if [ -z "$DEFINITION_PATH" ]; then
      usage
    elif [ ! -e "$DEFINITION_PATH" ]; then
      BUILTIN_DEFINITION_PATH="${RUBY_BUILD_ROOT}/share/ruby-build/${DEFINITION_PATH}"
      if [ -e "$BUILTIN_DEFINITION_PATH" ]; then
        DEFINITION_PATH="$BUILTIN_DEFINITION_PATH"
      else
        echo "ruby-build: definition not found: ${DEFINITION_PATH}" >&2
        exit 1
      fi
    fi

    PREFIX_PATH="$2"
    if [ -z "$PREFIX_PATH" ]; then
      usage
    fi

    if [ -z "$TMPDIR" ]; then
      TMP="/tmp"
    else
      TMP="${TMPDIR%/}"
    fi

    SEED="$(date "+%Y%m%d%H%M%S").$$"
    LOG_PATH="${TMP}/ruby-build.${SEED}.log"
    TEMP_PATH="${TMP}/ruby-build.${SEED}"
    RUBY_BIN="${PREFIX_PATH}/bin/ruby"
    CWD="$(pwd)"

    exec 4<> "$LOG_PATH" # open the log file at fd 4
    if [ -n "$VERBOSE" ]; then
      tail -f "$LOG_PATH" &
      trap "kill 0" SIGINT SIGTERM EXIT
    fi

    export LDFLAGS="-L'${PREFIX_PATH}/lib' ${LDFLAGS}"
    export CPPFLAGS="-I'${PREFIX_PATH}/include' ${CPPFLAGS}"

    unset RUBYOPT
    unset RUBYLIB

    trap build_failed ERR
    mkdir -p "$TEMP_PATH"
    source "$DEFINITION_PATH"
    rm -fr "$TEMP_PATH"
    trap - ERR