#!/bin/sh # # Copyright (c) 2018, Julian Ospald # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ## global variables ## VERSION=0.0.1 SCRIPT="$(basename "$0")" VERBOSE=false FORCE=false INSTALL_BASE="$HOME/.ghcup" GHC_LOCATION="$INSTALL_BASE/ghc" BIN_LOCATION="$INSTALL_BASE/bin" DOWNLOADER="curl" DOWNLOADER_OPTS="--fail -O" SCRIPT_UPDATE_URL="https://raw.githubusercontent.com/hasufell/ghcup/master/ghcup" GHC_DOWNLOAD_BASEURL="https://downloads.haskell.org/~ghc" ## print help ## usage() { (>&2 echo "ghcup ${VERSION} GHC up toolchain installer USAGE: ${SCRIPT} [FLAGS] FLAGS: -v, --verbose Enable verbose output -h, --help Prints help information -V, --version Prints version information SUBCOMMANDS: install Install GHC show Show current/installed GHC set Set currently active GHC version self-update Update this script in-place rm Remove an already installed GHC DISCUSSION: ghcup installs the Glasgow Haskell Compiler from the official release channels, enabling you to easily switch between different versions. ") exit 1 } install_usage() { (>&2 echo "ghcup-install Install the specified GHC version USAGE: ${SCRIPT} install [FLAGS] FLAGS: -h, --help Prints help information -f, --force Overwrite already existing installation ARGS: E.g. \"8.4.3\" or \"8.6.1\" DISCUSSION: Installs the specified GHC version into a self-contained \"~/.ghcup/ghc/\" directory and symlinks the ghc binaries to \"~/.ghcup/bin/-\". ") exit 1 } set_usage() { (>&2 echo "ghcup-set Set the currently active GHC to the specified version USAGE: ${SCRIPT} set [FLAGS] FLAGS: -h, --help Prints help information ARGS: E.g. \"8.4.3\" or \"8.6.1\" DISCUSSION: Sets the the current GHC version by creating non-versioned symlinks for all ghc binaries of the specified version in \"~/.ghcup/bin/\". ") exit 1 } self_update_usage() { (>&2 echo "ghcup-self-update Update the ghcup script in-place USAGE: ${SCRIPT} self-update [FLAGS] [TARGET-LOCATION] FLAGS: -h, --help Prints help information ARGS: [TARGET-LOCATION] Where to place the updated script (defaults to ~/.local/bin). ") exit 1 } show_usage() { (>&2 echo "ghcup-show Show the installed/current GHC versions USAGE: ${SCRIPT} show [FLAGS] FLAGS: -h, --help Prints help information -i, --installed Show installed GHC version only ") exit 1 } rm_usage() { (>&2 echo "ghcup-rm Remove the given GHC version installed by ghcup USAGE: ${SCRIPT} rm [FLAGS] FLAGS: -h, --help Prints help information ARGS: E.g. \"8.4.3\" or \"8.6.1\" ") exit 1 } ## utilities ## die() { (>&2 red_message "$1") exit 2 } edo() { if ${VERBOSE} ; then printf "\\033[0;34m%s\\033[0m\\n" "$*" 1>&2 fi "$@" || exit 2 } debug_message() { if ${VERBOSE} ; then printf "\\033[0;34m%s\\033[0m\\n" "$1" else if [ -n "$2" ] ; then printf "\\033[0;34m%s\\033[0m\\n" "$2" fi fi } optionv() { if ${VERBOSE} ; then echo "$1" else if [ -n "$2" ] ; then echo "$2" fi fi } status_message() { printf "\\033[0;32m%s\\033[0m\\n" "$1" } warning_message() { printf "\\033[1;33m%s\\033[0m\\n" "$1" } red_message() { printf "\\033[0;31m%s\\033[0m\\n" "$1" } get_distro_name() { if [ -f /etc/os-release ]; then # freedesktop.org and systemd # shellcheck disable=SC1091 . /etc/os-release printf "%s" "$NAME" elif command -V lsb_release >/dev/null 2>&1; then # linuxbase.org printf "%s" "$(lsb_release -si)" elif [ -f /etc/lsb-release ]; then # For some versions of Debian/Ubuntu without lsb_release command # shellcheck disable=SC1091 . /etc/lsb-release printf "%s" "$DISTRIB_ID" elif [ -f /etc/debian_version ]; then # Older Debian/Ubuntu/etc. printf "Debian" else # Fall back to uname, e.g. "Linux ", also works for BSD, etc. printf "%s" "$(uname -s)" fi } get_distro_ver() { if [ -f /etc/os-release ]; then # freedesktop.org and systemd # shellcheck disable=SC1091 . /etc/os-release printf "%s" "$VERSION_ID" elif command -V lsb_release >/dev/null 2>&1; then # linuxbase.org printf "%s" "$(lsb_release -sr)" elif [ -f /etc/lsb-release ]; then # For some versions of Debian/Ubuntu without lsb_release command # shellcheck disable=SC1091 . /etc/lsb-release printf "%s" "$DISTRIB_RELEASE" elif [ -f /etc/debian_version ]; then # Older Debian/Ubuntu/etc. printf "%s" "$(cat /etc/debian_version)" else # Fall back to uname, e.g. "Linux ", also works for BSD, etc. printf "%s" "$(uname -r)" fi } get_arch() { myarch=$(uname -m) case "${myarch}" in x86_64) printf "x86_64" # or AMD64 or Intel64 or whatever ;; i*86) printf "i386" # or IA32 or Intel32 or whatever ;; *) die "Cannot figure out architecture (was: ${myarch})" ;; esac unset myarch } # @FUNCTION: get_download_url # @USAGE: # @DESCRIPTION: # Gets the right (hopefully) download url for the given ghc version # and the current distro and architecture (which it tries to discover). # @STDOUT: ghc download url get_download_url() { [ -z "$1" ] && die "Internal error: no argument given to get_download_url" myghcver=$1 myarch=$(get_arch) mydistro=$(get_distro_name) mydistrover=$(get_distro_ver) # TODO: awkward, restructure case "${mydistro},${mydistrover},${myarch},${myghcver}" in Debian,7,i386,8.2.2) printf "%s" "${GHC_DOWNLOAD_BASEURL}/${myghcver}/ghc-${myghcver}-${myarch}-deb${mydistrover}-linux.tar.xz" ;; *,*,i386,*) printf "%s" "${GHC_DOWNLOAD_BASEURL}/${myghcver}/ghc-${myghcver}-${myarch}-deb8-linux.tar.xz" ;; Debian,*,*,8.2.2) printf "%s" "${GHC_DOWNLOAD_BASEURL}/${myghcver}/ghc-${myghcver}-${myarch}-deb8-linux.tar.xz" ;; Debian,8,*,*) printf "%s" "${GHC_DOWNLOAD_BASEURL}/${myghcver}/ghc-${myghcver}-${myarch}-deb8-linux.tar.xz" ;; Debian,*,*,*) printf "%s" "${GHC_DOWNLOAD_BASEURL}/${myghcver}/ghc-${myghcver}-${myarch}-deb9-linux.tar.xz" ;; Ubuntu,*,*,8.2.2) printf "%s" "${GHC_DOWNLOAD_BASEURL}/${myghcver}/ghc-${myghcver}-${myarch}-deb8-linux.tar.xz" ;; Ubuntu,*,*,*) printf "%s" "${GHC_DOWNLOAD_BASEURL}/${myghcver}/ghc-${myghcver}-${myarch}-deb9-linux.tar.xz" ;; *,*,*,8.2.2) printf "%s" "${GHC_DOWNLOAD_BASEURL}/${myghcver}/ghc-${myghcver}-${myarch}-deb8-linux.tar.xz" ;; *,*,*,*) # this is our best guess printf "%s" "${GHC_DOWNLOAD_BASEURL}/${myghcver}/ghc-${myghcver}-${myarch}-fedora27-linux.tar.xz" ;; esac unset myghcver myarch mydistro mydistrover } # @FUNCTION: ghc_already_installed # @USAGE: # @DESCRIPTION: # Checks whether the specified GHC version # has been installed by ghcup already. # @RETURN: 0 if GHC is already installed, 1 otherwise ghc_already_installed() { [ -z "$1" ] && die "Internal error: no argument given to ghc_already_installed" if [ -e "$(get_ghc_location "$1")" ] ; then return 0 else return 1 fi } # @FUNCTION: get_ghc_location # @USAGE: # @DESCRIPTION: # Gets/prints the location where the specified GHC is or would be installed. # Doesn't check whether that directory actually exist. Use # 'ghc_already_installed' for that. # @STDOUT: ghc location get_ghc_location() { [ -z "$1" ] && die "Internal error: no argument given to get_ghc_location" myghcver=$1 inst_location=${GHC_LOCATION}/${myghcver} printf "%s" "${inst_location}" unset myghcver inst_location } # @FUNCTION: download # @USAGE: # @DESCRIPTION: # Downloads the given url as a file into the current directory. # @RETURN: status code from the downloader download() { [ -z "$1" ] && die "Internal error: no argument given to download" # shellcheck disable=SC2086 ${DOWNLOADER} ${DOWNLOADER_OPTS} "$1" return $? } ## subcommand install ## # @FUNCTION: install_ghc # @USAGE: # @DESCRIPTION: # Installs the given ghc version with a lot of side effects. install_ghc() { [ -z "$1" ] && die "Internal error: no argument given to install_ghc" myghcver=$1 inst_location=$(get_ghc_location "$1") download_url=$(get_download_url "${myghcver}") download_tarball_name=$(basename "${download_url}") if ghc_already_installed "${myghcver}" ; then if ${FORCE} ; then echo "GHC already installed in ${inst_location}, overwriting!" else die "GHC already installed in ${inst_location}, use --force to overwrite" fi fi status_message "Installing GHC for $(get_distro_name) on architecture $(get_arch)" tmp_dir=$(mktemp -d) [ -z "${tmp_dir}" ] && die "Failed to create temporary directory" ( edo cd "${tmp_dir}" edo download "${download_url}" edo tar -xf ghc-*-linux.tar.xz edo cd "ghc-${myghcver}" debug_message "Installing GHC into ${inst_location}" edo ./configure --prefix="${inst_location}" edo make install # clean up edo cd .. [ -e "${tmp_dir}/${download_tarball_name}" ] && rm "${tmp_dir}/${download_tarball_name}" [ -e "${tmp_dir}/ghc-${myghcver}" ] && rm -r "${tmp_dir}/ghc-${myghcver}" ) || { [ -e "${tmp_dir}/${download_tarball_name}" ] && rm "${tmp_dir}/${download_tarball_name}" [ -e "${tmp_dir}/ghc-${myghcver}" ] && rm -r "${tmp_dir}/ghc-${myghcver}" die "Failed to install, consider updating this script via: ${SCRIPT} self-update" } [ -e "${BIN_LOCATION}" ] || mkdir "${BIN_LOCATION}" for f in "${inst_location}"/bin/*-"${myghcver}" ; do [ -e "${f}" ] || die "Something went wrong, ${f} does not exist!" fn=$(basename "${f}") # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}/bin/${fn}" "${BIN_LOCATION}/${fn}" unset fn done # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}"/bin/runhaskell "${BIN_LOCATION}/runhaskell-${myghcver}" status_message "Done installing, run \"ghci-${myghcver}\" or set up your current GHC via: ${SCRIPT} set-ghc ${myghcver}" unset myghcver inst_location f download_url download_tarball_name } ## subcommand set-ghc ## # @FUNCTION: set_ghc # @USAGE: # @DESCRIPTION: # Sets the current ghc version by creating symlinks. set_ghc() { [ -z "$1" ] && die "Internal error: no argument given to set_ghc" myghcver=$1 inst_location=$(get_ghc_location "$1") [ -e "${inst_location}" ] || die "GHC ${myghcver} not installed yet, use: ${SCRIPT} install ${myghcver}" [ -e "${BIN_LOCATION}" ] || edo mkdir "${BIN_LOCATION}" status_message "Setting GHC to ${myghcver}" for f in "${inst_location}"/bin/*-"${myghcver}" ; do [ -e "${f}" ] || die "Something went wrong, ${f} does not exist!" source_fn=$(basename "${f}") target_fn=$(echo "${source_fn}" | sed "s#-${myghcver}##") # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf ../ghc/"${myghcver}/bin/${source_fn}" "${BIN_LOCATION}/${target_fn}" unset source_fn target_fn done # shellcheck disable=SC2046 edo ln $(optionv "-v") -sf runghc "${BIN_LOCATION}"/runhaskell status_message "Done, make sure \"${BIN_LOCATION}\" is in your PATH!" unset myghcver inst_location f } ## self-update subcommand ## # @FUNCTION: self_update # @USAGE: # @DESCRIPTION: # Downloads the latest version of this script and places it into # the given directory. self_update() { target_location=$1 [ -e "${target_location}" ] || die "Destination \"${target_location}\" does not exist, cannot update script" status_message "Updating ${SCRIPT}" ( edo cd "${target_location}" edo download "${SCRIPT_UPDATE_URL}" edo chmod +x "${target_location}"/ghcup ) || die "failed to install" status_message "Done, make sure \"${target_location}\" is in your PATH!" unset target_location } ## show subcommand ## # @FUNCTION: show_ghc # @DESCRIPTION: # Prints the currently installed and selected GHC, in human-friendly # format. show_ghc() { current_ghc=$(show_ghc_installed) echo "Installed GHCs:" for i in "${GHC_LOCATION}"/* ; do [ -e "${i}" ] || die "Something went wrong, ${i} does not exist!" echo " $(basename "${i}")" done if [ -n "${current_ghc}" ] ; then echo echo "Current GHC" echo " ${current_ghc}" fi unset current_ghc i } # @FUNCTION: show_ghc_installed # @DESCRIPTION: # Prints the currently selected GHC only as version string. # @STDOUT: current GHC version show_ghc_installed() { current_ghc="${BIN_LOCATION}/ghc" real_ghc=$(realpath "${current_ghc}" 2>/dev/null) if [ -L "${current_ghc}" ] ; then # is symlink if [ -e "${real_ghc}" ] ; then # exists (realpath was called) real_ghc="$(basename "${real_ghc}" | sed 's#ghc-##')" printf "%s" "${real_ghc}" else # is a broken symlink red_message "broken symlink" fi fi unset real_ghc current_ghc } ## rm subcommand ## # @FUNCTION: rm_ghc # @USAGE: # @DESCRIPTION: # Removes the given GHC version installed by ghcup. rm_ghc() { [ -z "$1" ] && die "Internal error: no argument given to rm_ghc" myghcver=$1 inst_location=$(get_ghc_location "${myghcver}") [ -z "${myghcver}" ] && die "We are paranoid, ghcver not set" if ghc_already_installed "${myghcver}" ; then for f in "${BIN_LOCATION}"/*-"${myghcver}" ; do # https://tanguy.ortolo.eu/blog/article113/test-symlink [ ! -e "${f}" ] && [ ! -h "${f}" ] && die "Something went wrong, ${f} does not exist!" edo rm "${f}" done edo rm -r "${inst_location}" else warning_message "${myghcver} doesn't appear to be installed, skipping" fi status_message "Successfully removed GHC ${myghcver}, you might have to" status_message "set the currently active GHC now to fix dangling symlinks:" status_message " ghcup set " unset myghcver inst_location f } ## command line parsing and entry point ## # sanity checks if [ -z "$HOME" ] ; then die "HOME env not set, cannot operate" fi [ $# -lt 1 ] && usage while [ $# -gt 0 ] ; do case $1 in -v|--verbose) VERBOSE=true shift 1;; -V|--version) printf "%s" "${VERSION}" exit 0;; -h|--help) usage;; *) case $1 in install) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) install_usage;; -f|--force) FORCE=true shift 1;; *) GHC_VER=$1 break;; esac done [ "${GHC_VER}" ] || install_usage install_ghc "${GHC_VER}" break;; set) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) set_usage;; *) GHC_VER=$1 break;; esac done [ "${GHC_VER}" ] || set_usage set_ghc "${GHC_VER}" break;; self-update) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) self_update_usage;; *) TARGET_LOCATION=$1 break;; esac done if [ "${TARGET_LOCATION}" ] ; then self_update "${TARGET_LOCATION}" else self_update "${HOME}/.local/bin" fi break;; show) SHOW_INSTALLED=false shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) show_usage;; -i|--installed) SHOW_INSTALLED=true break;; *) show_usage;; esac done if ${SHOW_INSTALLED} ; then show_ghc_installed else show_ghc fi break;; rm) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) rm_usage;; *) GHC_VER=$1 break;; esac done [ "${GHC_VER}" ] || rm_usage rm_ghc "${GHC_VER}" break;; *) usage;; esac break;; esac done