#!/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. # TODO: # - make removal more robust ########################## #--[ Global Variables ]--# ########################## # @VARIABLE: VERSION # @DESCRIPTION: # Version of this script. VERSION=0.0.5 # @VARIABLE: SCRIPT # @DESCRIPTION: # Name of this script. SCRIPT="$(basename "$0")" # @VARIABLE: VERBOSE # @DESCRIPTION: # Whether to print verbose messages in this script. VERBOSE=false # @VARIABLE: FORCE # @DESCRIPTION: # Whether to force installation and overwrite files. FORCE=false # @VARIABLE: INSTALL_BASE # @DESCRIPTION: # The main install directory where all ghcup stuff happens. INSTALL_BASE="$HOME/.ghcup" # @VARIABLE: GHC_LOCATION # @DESCRIPTION: # The location where ghcup will install different ghc versions. # This is expected to be a subdirectory of INSTALL_BASE. GHC_LOCATION="$INSTALL_BASE/ghc" # @VARIABLE: BIN_LOCATION # @DESCRIPTION: # The location where ghcup will create symlinks for GHC binaries. # This is expected to be a subdirectory of INSTALL_BASE. BIN_LOCATION="$INSTALL_BASE/bin" # @VARIABLE: CACHE_LOCATION # @DESCRIPTION: # The location where ghcup will put tarballs for caching. # This is expected to be a subdirectory of INSTALL_BASE. CACHE_LOCATION="$INSTALL_BASE/cache" # @VARIABLE: DOWNLOADER # @DESCRIPTION: # What program to use for downloading files. DOWNLOADER="curl" # @VARIABLE: DOWNLOADER_OPTS # @DESCRIPTION: # Options passed to the download program. DOWNLOADER_OPTS="--fail -O" # @VARIABLE: SCRIPT_UPDATE_URL # @DESCRIPTION: # Location to update this script from. SCRIPT_UPDATE_URL="https://raw.githubusercontent.com/haskell/ghcup/master/ghcup" # @VARIABLE: GHC_DOWNLOAD_BASEURL # @DESCRIPTION: # Base URL for all GHC tarballs. GHC_DOWNLOAD_BASEURL="https://downloads.haskell.org/~ghc" # @VARIABLE: KNOWN_GOOD_CABAL # @DESCRIPTION: # The latests known good cabal-install version for # which a pre-built binary exists. KNOWN_GOOD_CABAL="2.4.0.0" # @VARIABLE: JOBS # @DESCRIPTION: # How many jobs to use for compiling GHC. JOBS="1" # @VARIABLE: SOURCE # @DESCRIPTION: # The $0 argument, which contains # the script name. SOURCE="$0" # @VARIABLE: CACHING # @DESCRIPTION: # Whether to cache tarballs in $CACHE_LOCATION. CACHING=false #################### #--[ Print Help ]--# #################### # @FUNCTION: usage # @DESCRIPTION: # Print the help message for 'ghcup' to STDERR # and exit the script with status code 1. 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 -w, --wget Use wget instead of curl -c, --cache Use \"${CACHE_LOCATION}\" for caching tarballs (these will not be removed by ghcup) SUBCOMMANDS: install Install GHC compile Compile and install GHC from source show Show current/installed GHC set Set currently active GHC version self-update Update this script in-place rm Remove an already installed GHC install-cabal Install cabal-install debug-info Print debug info (e.g. detected system/distro) DISCUSSION: ghcup installs the Glasgow Haskell Compiler from the official release channels, enabling you to easily switch between different versions. ") exit 1 } # @FUNCTION: install_usage # @DESCRIPTION: # Print the help message for 'ghcup install' to STDERR # and exit the script with status code 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 } # @FUNCTION: set_usage # @DESCRIPTION: # Print the help message for 'ghcup set' to STDERR # and exit the script with status code 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 } # @FUNCTION: self_update_usage # @DESCRIPTION: # Print the help message for 'ghcup self-update' to STDERR # and exit the script with status code 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 directory of the script). ") exit 1 } # @FUNCTION: show_usage # @DESCRIPTION: # Print the help message for 'ghcup show' to STDERR # and exit the script with status code 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 } # @FUNCTION: rm_usage # @DESCRIPTION: # Print the help message for 'ghcup rm' to STDERR # and exit the script with status code 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 -f, --force Don't prompt user ARGS: E.g. \"8.4.3\" or \"8.6.1\" ") exit 1 } # @FUNCTION: install_cabal_usage # @DESCRIPTION: # Print the help message for 'ghcup install-cabal' to STDERR # and exit the script with status code 1. install_cabal_usage() { (>&2 echo "ghcup-install-cabal Install the specified or a default cabal version USAGE: ${SCRIPT} install-cabal [FLAGS] [VERSION] FLAGS: -h, --help Prints help information ARGS: E.g. \"2.4.0.0\" DISCUSSION: Installs the specified cabal-install version (or the default ${KNOWN_GOOD_CABAL}) into \"${BIN_LOCATION}\", so it can be overwritten by later \"cabal new-install cabal-install\", which installs into \"~/.cabal/bin\". Make sure to set up your PATH appropriately, so the cabal installation takes precedence. ") exit 1 } # @FUNCTION: compile_usage # @DESCRIPTION: # Print the help message for 'ghcup compile' to STDERR # and exit the script with status code 1. compile_usage() { (>&2 echo "ghcup-compile Compile and install the specified GHC version USAGE: ${SCRIPT} compile [FLAGS] FLAGS: -h, --help Prints help information -f, --force Overwrite already existing installation -j, --jobs How many jobs for compilation -c, --build-config Use the given config file as build config ARGS: E.g. \"8.4.3\" or \"8.6.1\" E.g. \"ghc-8.2.2\" or a full path DISCUSSION: Compiles and installs the specified GHC version into a self-contained \"~/.ghcup/ghc/\" directory and symlinks the ghc binaries to \"~/.ghcup/bin/-\". EXAMPLE: ghcup -v compile -f -j 4 8.4.2 ghc-8.2.2 ") exit 1 } # @FUNCTION: debug_info_usage # @DESCRIPTION: # Print the help message for 'ghcup debug-info' to STDERR # and exit the script with status code 1. debug_info_usage() { (>&2 echo "ghcup-debug-info Print debug info (e.g. detected system/distro) USAGE: ${SCRIPT} debug-info FLAGS: -h, --help Prints help information DISCUSSION: Prints debug information, e.g. detected system architecture, distribution, version, as well as script variables. This is mainly useful for debugging purposes. ") exit 1 } ########################### #--[ Utility functions ]--# ########################### # @FUNCTION: die # @USAGE: [msg] # @DESCRIPTION: # Exits the shell script with status code 2 # and prints the given message in red to STDERR, if any. die() { (>&2 red_message "$1") exit 2 } # @FUNCTION: edo # @USAGE: # @DESCRIPTION: # Executes the given command. Also prints what # command that is (in blue) if verbosity is enabled. # Exits with status code 2 if the command failed. edo() { if ${VERBOSE} ; then printf "\\033[0;34m%s\\033[0m\\n" "$*" 1>&2 fi "$@" || exit 2 } # @FUNCTION: debug_message # @USAGE: # @DESCRIPTION: # Print a blue debug message if verbosity is enabled. debug_message() { if ${VERBOSE} ; then printf "\\033[0;34m%s\\033[0m\\n" "$1" fi } # @FUNCTION: optionv # @USAGE: [arg2] # @DESCRIPTION: # If verbosity is enabled, echo the first argument, otherwise # the second (if any). # @STDOUT: first or second argument optionv() { if ${VERBOSE} ; then echo "$1" else if [ -n "$2" ] ; then echo "$2" fi fi } # @FUNCTION: status_message # @USAGE: # @DESCRIPTION: # Print a green status message. status_message() { printf "\\033[0;32m%s\\033[0m\\n" "$1" } # @FUNCTION: warning_message # @USAGE: # @DESCRIPTION: # Print a yellow warning message. warning_message() { printf "\\033[1;33m%s\\033[0m\\n" "$1" } # @FUNCTION: red_message # @USAGE: # @DESCRIPTION: # Print a red message. red_message() { printf "\\033[0;31m%s\\033[0m\\n" "$1" } # @FUNCTION: get_distro_name # @DESCRIPTION: # Gets the current distro identifier following # https://unix.stackexchange.com/a/6348 # @STDOUT: current distro identifier 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 } # @FUNCTION: get_distro_ver # @DESCRIPTION: # Gets the current distro version (if any) following # https://unix.stackexchange.com/a/6348 # @STDOUT: current distro version, if any 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 } # @FUNCTION: get_arch # @DESCRIPTION: # Gets the architecture following # https://unix.stackexchange.com/a/6348 # Fails for any architecture that we don't know a GHC version for. # @STDOUT: current architecture 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. download() { [ -z "$1" ] && die "Internal error: no argument given to download" # shellcheck disable=SC2086 edo ${DOWNLOADER} ${DOWNLOADER_OPTS} "$1" } # @FUNCTION: unpack # @USAGE: # @DESCRIPTION: # Uncompresses and unpacks the given tarball if needed by discovering the # file extension. unpack() { [ -z "$1" ] && die "Internal error: no argument given to unpack" filename=$1 file_ext=${filename##*.} # this is for portability, since not all # distros have tar with compression detection # capability case "${file_ext}" in xz) debug_message "xz -cd \"${filename}\" | tar -xf -" ( xz -cd "${filename}" | tar -xf - ; ) || die "unpacking failed!" ;; gz) debug_message "gzip -cd \"${filename}\" | tar -xf -" ( gzip -cd "${filename}" | tar -xf - ; ) || die "unpacking failed!" ;; tar) edo tar -xf "${filename}" ;; *) die "Unknown file extension: \"${file_ext}\"" esac unset filename file_ext } # @FUNCTION: ask_for_confirmation # @USAGE: [confirmation-msg] # @DESCRIPTION: # Asks the user for confirmation and returns 0 for yes, 1 for no. # @RETURN: 0 if user confirmed, 1 otherwise ask_for_confirmation() { confirmation_msg=$1 if [ -n "${confirmation_msg}" ] ; then printf "%s\\n(y/n and press Enter)\\n" "${confirmation_msg}" else printf "Confirm action: (y/n and press Enter)\\n" fi read -r answer if [ "${answer}" != "${answer#[Yy]}" ] ;then return 0 else return 1 fi unset confirmation_msg answer } ############################ #--[ 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}") first_install=true 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 first_install=false 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" ( if ${CACHING} ; then [ -e "${CACHE_LOCATION}" ] || { [ -e "${INSTALL_BASE}" ] || edo mkdir "${INSTALL_BASE}" edo mkdir "${CACHE_LOCATION}" } if [ ! -e "${CACHE_LOCATION}/${download_tarball_name}" ] ; then edo cd "${CACHE_LOCATION}" download "${download_url}" fi edo cd "${tmp_dir}" unpack "${CACHE_LOCATION}/${download_tarball_name}" else edo cd "${tmp_dir}" download "${download_url}" unpack "${download_tarball_name}" fi 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}" if ${first_install} ; then [ -e "${inst_location}" ] && rm -r "${inst_location}" else warning_message "GHC force installation failed. The install might be broken." warning_message "Consider running: ghcup rm ${myghcver}" fi 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 ${myghcver}" unset myghcver inst_location f download_url download_tarball_name first_install tmp_dir } ######################## #--[ Subcommand set ]--# ######################## # @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 } ################################ #--[ Subcommand self-update ]--# ################################ # @FUNCTION: script_dir # @DESCRIPTION: # Portably gets the full directory of where # this script resides in and prints it to stdout. # @STDOUT: script directory script_dir() { mysource=${SOURCE} while [ -h "${mysource}" ]; do mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null && pwd )" mysource="$(readlink "${mysource}")" [ "${mysource%${mysource#?}}"x != '/x' ] && mysource="${mydir}/${mysource}" done mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null && pwd )" echo "${mydir}" unset mysource mydir } # @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 "$(mktemp -d)" download "${SCRIPT_UPDATE_URL}" edo chmod +x ghcup edo mv -f ghcup "${target_location}"/ghcup ) || die "failed to install" status_message "Done, make sure \"${target_location}\" is in your PATH!" unset target_location } ######################### #--[ Subcommand show ]--# ######################### # @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 if [ -e "${i}" ] ; then echo " $(basename "${i}")" else # directory is empty echo " None" exit 0 fi 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 } ####################### #--[ Subcommand rm ]--# ####################### # @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 [ -z "${inst_location}" ] && die "internal error: inst_location empty!" if ! ${FORCE} ; then if ! ask_for_confirmation "Really removing ${myghcver}? This will also recursively remove the following directory (please double-check): \"${inst_location}\"" ; then warning_message "Not removing GHC..." return 0 fi fi for f in "${BIN_LOCATION}"/*-"${myghcver}" ; do # https://tanguy.ortolo.eu/blog/article113/test-symlink [ ! -e "${f}" ] && [ ! -h "${f}" ] && { warning_message "No existing symlinks for ${myghcver} in ${BIN_LOCATION}, skipping" break } edo rm "${f}" done edo rm -r "${inst_location}" status_message "Successfully removed GHC ${myghcver}." if [ -h "${BIN_LOCATION}/ghc" ] && [ ! -e "${BIN_LOCATION}/ghc" ] ; then warning_message "Currently active GHC is a dangling symlink, removing..." # TODO: known_tools is not very robust, but we want to avoid accidentially deleting # unrelated things (even if those are dangling symlinks) known_tools="ghc ghci ghc-pkg haddock-ghc runghc runhaskell" # remove dangling symlinks for ghc, ghci, ... for t in ${known_tools} ; do if [ -h "${BIN_LOCATION}/${t}" ] && [ ! -e "${BIN_LOCATION}/${t}" ] ; then edo rm "${BIN_LOCATION}/${t}" fi done unset t known_tools warning_message "Done." warning_message "You may now want to set currently active GHC to a different version via:" warning_message " ghcup set " fi else warning_message "${myghcver} doesn't appear to be installed, skipping" fi unset myghcver inst_location f } ############################ #--[ Subcommand install ]--# ############################ # @FUNCTION: install_cabal # @USAGE: # @DESCRIPTION: # Installs the given cabal version. install_cabal() { [ -z "$1" ] && die "Internal error: no argument given to install_cabal" mycabalver=$1 myarch=$(get_arch) inst_location=$BIN_LOCATION download_url="https://downloads.haskell.org/~cabal/cabal-install-${mycabalver}/cabal-install-${mycabalver}-${myarch}-unknown-linux.tar.gz" download_tarball_name=$(basename "${download_url}") [ -e "${inst_location}" ] || { # TODO: this is a bit shaky because we don't use -p edo mkdir "${INSTALL_BASE}" edo mkdir "${BIN_LOCATION}" } tmp_dir=$(mktemp -d) [ -z "${tmp_dir}" ] && die "Failed to create temporary directory" ( if ${CACHING} ; then [ -e "${CACHE_LOCATION}" ] || { [ -e "${INSTALL_BASE}" ] || edo mkdir "${INSTALL_BASE}" edo mkdir "${CACHE_LOCATION}" } if [ ! -e "${CACHE_LOCATION}/${download_tarball_name}" ] ; then edo cd "${CACHE_LOCATION}" download "${download_url}" fi edo cd "${tmp_dir}" unpack "${CACHE_LOCATION}/${download_tarball_name}" else edo cd "${tmp_dir}" download "${download_url}" unpack "${download_tarball_name}" fi edo mv -f cabal "${inst_location}"/cabal if [ -e "${tmp_dir}/${download_tarball_name}" ] ; then rm "${tmp_dir}/${download_tarball_name}" fi ) || die "Failed to install cabal-install" status_message "Successfully installed cabal-install into" status_message " ${BIN_LOCATION}" status_message "" status_message "You may want to run the following to get the really latest version:" status_message " cabal new-install cabal-install" status_message "" status_message "And make sure that \"~/.cabal/bin\" comes *before* \"${BIN_LOCATION}\"" status_message "in your PATH!" unset mycabalver myarch inst_location download_url download_tarball_name tmp_dir } # @FUNCTION: compile_ghc # @USAGE: [build.mk] # @DESCRIPTION: # Compile and installs the given GHC version with the # specified GHC bootstrap version. # Can additionally take a custom file that will be used # as build configuration. compile_ghc() { { [ -z "$1" ] || [ -z "$2" ] ;} && die "Internal error: not enough arguments given to compile_ghc" myghcver=$1 bootstrap_ghc=$2 inst_location=$(get_ghc_location "$1") download_url="https://downloads.haskell.org/~ghc/${myghcver}/ghc-${myghcver}-src.tar.xz" download_tarball_name=$(basename "${download_url}") if [ -n "$3" ] ; then case "$3" in /*) build_config=$3 ;; *) build_config="$(pwd)/$3" ;; esac [ -e "${build_config}" ] || die "specified build config \"${build_config}\" file does not exist!" fi 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 "Compiling GHC for version ${myghcver} from source" tmp_dir=$(mktemp -d) [ -z "${tmp_dir}" ] && die "Failed to create temporary directory" ( if ${CACHING} ; then [ -e "${CACHE_LOCATION}" ] || { [ -e "${INSTALL_BASE}" ] || edo mkdir "${INSTALL_BASE}" edo mkdir "${CACHE_LOCATION}" } if [ ! -e "${CACHE_LOCATION}/${download_tarball_name}" ] ; then edo cd "${CACHE_LOCATION}" download "${download_url}" fi edo cd "${tmp_dir}" unpack "${CACHE_LOCATION}/${download_tarball_name}" else edo cd "${tmp_dir}" download "${download_url}" unpack "${download_tarball_name}" fi edo cd "ghc-${myghcver}" if [ -n "${build_config}" ] ; then edo cat "${build_config}" > mk/build.mk else cat <<-EOF > mk/build.mk || die V=0 BUILD_MAN = NO BUILD_SPHINX_HTML = NO BUILD_SPHINX_PDF = NO HADDOCK_DOCS = YES GhcWithLlvmCodeGen = YES EOF fi edo ./boot edo ./configure --prefix="${inst_location}" --with-ghc="${bootstrap_ghc}" edo make -j${JOBS} 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 Also check https://ghc.haskell.org/trac/ghc/wiki/Building/Preparation/Linux for build requirements and follow the instructions." } [ -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 ${myghcver}" unset myghcver bootstrap_ghc inst_location f download_url download_tarball_name tmp_dir } ############################### #--[ Subcommand debug-info ]--# ############################### # @FUNCTION: print_debug_info # @DESCRIPTION: # Print debug info (e.g. detected system/distro). print_debug_info() { echo "Script version: ${VERSION}" echo echo "Script variables:" echo " GHC install location: ${GHC_LOCATION}" echo " Binary install location: ${BIN_LOCATION}" echo " Downloader: ${DOWNLOADER} ${DOWNLOADER_OPTS} " echo " Script update url: ${SCRIPT_UPDATE_URL}" echo " GHC download baseurl: ${GHC_DOWNLOAD_BASEURL}" echo " Known good cabal version: ${KNOWN_GOOD_CABAL}" echo echo "Detected system information:" echo " Architecture: $(get_arch)" echo " Distribution: $(get_distro_name)" echo " Distro version: $(get_distro_ver)" } ####################### #--[ Sanity checks ]--# ####################### if [ -z "$HOME" ] ; then die "HOME env not set, cannot operate" fi ############################################## #--[ Command line parsing and entry point ]--# ############################################## [ $# -lt 1 ] && usage while [ $# -gt 0 ] ; do case $1 in -v|--verbose) VERBOSE=true shift 1 if [ $# -lt 1 ] ; then usage fi ;; -V|--version) printf "%s" "${VERSION}" exit 0;; -h|--help) usage;; -w|--wget) DOWNLOADER="wget" DOWNLOADER_OPTS="" shift 1 if [ $# -lt 1 ] ; then usage fi ;; -c|--cache) CACHING=true shift 1 if [ $# -lt 1 ] ; then usage fi ;; *) # TODO: here comes command availability checking 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 "$(script_dir)" 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;; -f|--force) FORCE=true shift 1;; *) GHC_VER=$1 break;; esac done [ "${GHC_VER}" ] || rm_usage rm_ghc "${GHC_VER}" break;; install-cabal) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) install_cabal_usage;; -f|--force) FORCE=true shift 1;; *) CABAL_VER=$1 break;; esac done if [ "${CABAL_VER}" ] ; then install_cabal "${CABAL_VER}" else install_cabal "${KNOWN_GOOD_CABAL}" fi break;; compile) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) compile_usage;; -f|--force) FORCE=true shift 1;; -j|--jobs) JOBS=$2 shift 2;; -c|--build-config) BUILD_CONFIG=$2 shift 2;; *) GHC_VER=$1 BOOTSTRAP_GHC=$2 break;; esac done [ "${GHC_VER}" ] || compile_usage [ "${BOOTSTRAP_GHC}" ] || compile_usage compile_ghc "${GHC_VER}" "${BOOTSTRAP_GHC}" "${BUILD_CONFIG}" break;; debug-info) shift 1 while [ $# -gt 0 ] ; do case $1 in -h|--help) debug_info_usage;; *) debug_info_usage;; esac done print_debug_info break;; *) usage;; esac break;; esac done # vim: tabstop=4 shiftwidth=4 expandtab