#!/bin/sh # This script downloads the 'ghcup' binary into '~/.ghcup/bin/' and then runs an interactive # installation that lets you choose various options. Below is a list of environment variables # that affect the installation procedure. # Main settings: # * BOOTSTRAP_HASKELL_NONINTERACTIVE - any nonzero value for noninteractive installation # * BOOTSTRAP_HASKELL_NO_UPGRADE - any nonzero value to not trigger the upgrade # * BOOTSTRAP_HASKELL_MINIMAL - any nonzero value to only install ghcup # * GHCUP_USE_XDG_DIRS - any nonzero value to respect The XDG Base Directory Specification # * BOOTSTRAP_HASKELL_VERBOSE - any nonzero value for more verbose installation # * BOOTSTRAP_HASKELL_GHC_VERSION - the ghc version to install # * BOOTSTRAP_HASKELL_CABAL_VERSION - the cabal version to install # * BOOTSTRAP_HASKELL_CABAL_XDG - don't disable the XDG logic (this doesn't force XDG though, because cabal is confusing) # * BOOTSTRAP_HASKELL_INSTALL_NO_STACK - disable installation of stack # * BOOTSTRAP_HASKELL_INSTALL_NO_STACK_HOOK - disable installation stack ghcup hook # * BOOTSTRAP_HASKELL_STACK_VERSION - the stack version to install # * BOOTSTRAP_HASKELL_INSTALL_HLS - whether to install hls # * BOOTSTRAP_HASKELL_HLS_VERSION - the hls version to install # * BOOTSTRAP_HASKELL_ADJUST_BASHRC - whether to adjust PATH in bashrc (prepend) # * BOOTSTRAP_HASKELL_ADJUST_CABAL_CONFIG - whether to adjust mingw paths in cabal.config on windows # * BOOTSTRAP_HASKELL_DOWNLOADER - which downloader to use (default: curl) # * GHCUP_BASE_URL - the base url for ghcup binary download (use this to overwrite https://downloads.haskell.org/~ghcup with a mirror) # * GHCUP_MSYS2_ENV - the msys2 environment to use on windows, see https://www.msys2.org/docs/environments/ (defaults to MINGW64, MINGW32 or CLANGARM64, depending on the architecture) # License: LGPL-3.0 # safety subshell to avoid executing anything in case this script is not downloaded properly ( die() { if [ -n "${NO_COLOR}" ] ; then (>&2 printf "%s\\n" "$1") else (>&2 printf "\\033[0;31m%s\\033[0m\\n" "$1") fi exit 2 } plat="$(uname -s)" arch=$(uname -m) ghver="0.1.50.2" : "${GHCUP_BASE_URL:=https://downloads.haskell.org/~ghcup}" export GHCUP_SKIP_UPDATE_CHECK=yes : "${BOOTSTRAP_HASKELL_DOWNLOADER:=curl}" set_msys2_env_dir() { case "${GHCUP_MSYS2_ENV}" in "") case "${arch}" in x86_64|amd64) GHCUP_MSYS2_ENV_DIR="mingw64" ;; i*86) GHCUP_MSYS2_ENV_DIR="mingw32" ;; aarch64|arm64) GHCUP_MSYS2_ENV_DIR="clangarm64" ;; *) die "Unknown architecture: ${arch}" ;; esac ;; MSYS) GHCUP_MSYS2_ENV_DIR="usr" ;; UCRT64) GHCUP_MSYS2_ENV_DIR="ucrt64" ;; CLANG64) GHCUP_MSYS2_ENV_DIR="clang64" ;; CLANGARM64) GHCUP_MSYS2_ENV_DIR="clangarm64" ;; CLANG32) GHCUP_MSYS2_ENV_DIR="clang32" ;; MINGW64) GHCUP_MSYS2_ENV_DIR="mingw64" ;; MINGW32) GHCUP_MSYS2_ENV_DIR="mingw32" ;; *) die "Invalid value for GHCUP_MSYS2_ENV. Valid values are: MSYS, UCRT64, CLANG64, CLANGARM64, CLANG32, MINGW64, MINGW32" ;; esac } case "${plat}" in MSYS*|MINGW*|CYGWIN*) : "${GHCUP_INSTALL_BASE_PREFIX:=/c}" GHCUP_DIR=$(cygpath -u "${GHCUP_INSTALL_BASE_PREFIX}/ghcup") GHCUP_BIN=$(cygpath -u "${GHCUP_INSTALL_BASE_PREFIX}/ghcup/bin") : "${GHCUP_MSYS2:=${GHCUP_DIR}/msys64}" set_msys2_env_dir ;; *) : "${GHCUP_INSTALL_BASE_PREFIX:=$HOME}" if [ -n "${GHCUP_USE_XDG_DIRS}" ] ; then GHCUP_DIR=${XDG_DATA_HOME:=$HOME/.local/share}/ghcup GHCUP_BIN=${XDG_BIN_HOME:=$HOME/.local/bin} else GHCUP_DIR=${GHCUP_INSTALL_BASE_PREFIX}/.ghcup GHCUP_BIN=${GHCUP_INSTALL_BASE_PREFIX}/.ghcup/bin fi ;; esac : "${BOOTSTRAP_HASKELL_GHC_VERSION:=recommended}" : "${BOOTSTRAP_HASKELL_CABAL_VERSION:=recommended}" : "${BOOTSTRAP_HASKELL_STACK_VERSION:=recommended}" : "${BOOTSTRAP_HASKELL_HLS_VERSION:=recommended}" warn() { if [ -n "${NO_COLOR}" ] ; then printf "%s\\n" "$1" else case "${plat}" in MSYS*|MINGW*|CYGWIN*) # shellcheck disable=SC3037 echo -e "\\033[0;35m$1\\033[0m" ;; *) printf "\\033[0;35m%s\\033[0m\\n" "$1" ;; esac fi } yellow() { if [ -n "${NO_COLOR}" ] ; then printf "%s\\n" "$1" else case "${plat}" in MSYS*|MINGW*|CYGWIN*) # shellcheck disable=SC3037 echo -e "\\033[0;33m$1\\033[0m" ;; *) printf "\\033[0;33m%s\\033[0m\\n" "$1" ;; esac fi } green() { if [ -n "${NO_COLOR}" ] ; then printf "%s\\n" "$1" else case "${plat}" in MSYS*|MINGW*|CYGWIN*) # shellcheck disable=SC3037 echo -e "\\033[0;32m$1\\033[0m" ;; *) printf "\\033[0;32m%s\\033[0m\\n" "$1" ;; esac fi } edo() { "$@" || die "\"$*\" failed!" } eghcup_raw() { "${GHCUP_BIN}/ghcup" "$@" || die "\"ghcup $*\" failed!" } eghcup() { _eghcup "$@" } _eghcup() { if [ -n "${BOOTSTRAP_HASKELL_YAML}" ] ; then args="-s ${BOOTSTRAP_HASKELL_YAML} --metadata-fetching-mode=Strict" else args="--metadata-fetching-mode=Strict" fi if [ -z "${BOOTSTRAP_HASKELL_VERBOSE}" ] ; then # shellcheck disable=SC2086 "${GHCUP_BIN}/ghcup" ${args} "$@" || die "\"ghcup ${args} $*\" failed!" else # shellcheck disable=SC2086 "${GHCUP_BIN}/ghcup" ${args} --verbose "$@" || die "\"ghcup ${args} --verbose $*\" failed!" fi } _ecabal() { # shellcheck disable=SC2317 if [ -n "${CABAL_BIN}" ] ; then "${CABAL_BIN}" "$@" else # shellcheck disable=SC2086 "${GHCUP_BIN}/cabal" "$@" fi } ecabal() { _ecabal "$@" || die "\"cabal $*\" failed!" } _done() { echo echo "===============================================================================" case "${plat}" in MSYS*|MINGW*|CYGWIN*) green green "All done!" green green "In a new powershell or cmd.exe session, now you can..." green green "Start a simple repl via:" green " ghci" green green "Start a new haskell project in the current directory via:" green " cabal init --interactive" green green "To install other GHC versions and tools, run:" green " ghcup tui" green green "To install system libraries and update msys2/mingw64," green "open the \"Mingw haskell shell\"" green "and the \"Mingw package management docs\"" green "desktop shortcuts." green green "If you are new to Haskell, check out https://www.haskell.org/ghcup/steps/" ;; *) green green "All done!" green green "To start a simple repl, run:" green " ghci" green green "To start a new haskell project in the current directory, run:" green " cabal init --interactive" green green "To install other GHC versions and tools, run:" green " ghcup tui" green green "If you are new to Haskell, check out https://www.haskell.org/ghcup/steps/" ;; esac exit 0 } # @FUNCTION: posix_realpath # @USAGE: # @DESCRIPTION: # Portably gets the realpath and prints it to stdout. # This was initially inspired by # https://gist.github.com/tvlooy/cbfbdb111a4ebad8b93e # and # https://stackoverflow.com/a/246128 # # If the file does not exist, just prints it appended to the current directory. # @STDOUT: realpath of the given file posix_realpath() { [ -z "$1" ] && die "Internal error: no argument given to posix_realpath" current_loop=0 max_loops=50 mysource=$1 # readlink and '[ -h $path ]' behave different wrt '/sbin/' and '/sbin', so we strip it mysource=${mysource%/} [ -z "${mysource}" ] && mysource=$1 while [ -h "${mysource}" ]; do current_loop=$((current_loop+1)) mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null 2>&1 && pwd )" mysource="$(readlink "${mysource}")" [ "${mysource%"${mysource#?}"}"x != '/x' ] && mysource="${mydir%/}/${mysource}" if [ ${current_loop} -gt ${max_loops} ] ; then (>&2 echo "${1}: Too many levels of symbolic links") echo "$1" return fi done mydir="$( cd -P "$( dirname "${mysource}" )" > /dev/null 2>&1 && pwd )" # TODO: better distinguish between "does not exist" and "permission denied" if [ -z "${mydir}" ] ; then (>&2 echo "${1}: Permission denied") echo "$(pwd)/$1" else echo "${mydir%/}/$(basename "${mysource}")" fi unset current_loop max_loops mysource mydir } download_ghcup() { case "${plat}" in "linux"|"Linux") case "${arch}" in x86_64|amd64) # we could be in a 32bit docker container, in which # case uname doesn't give us what we want if [ "$(getconf LONG_BIT)" = "32" ] ; then _url=${GHCUP_BASE_URL}/${ghver}/i386-linux-ghcup-${ghver} elif [ "$(getconf LONG_BIT)" = "64" ] ; then _url=${GHCUP_BASE_URL}/${ghver}/x86_64-linux-ghcup-${ghver} else die "Unknown long bit size: $(getconf LONG_BIT)" fi ;; i*86) _url=${GHCUP_BASE_URL}/${ghver}/i386-linux-ghcup-${ghver} ;; armv7*|*armv8l*) # later versions don't support armv7 anymore _url=${GHCUP_BASE_URL}/0.1.40.0/armv7-linux-ghcup-0.1.40.0 ;; aarch64|arm64) # we could be in a 32bit docker container, in which # case uname doesn't give us what we want if [ "$(getconf LONG_BIT)" = "32" ] ; then # later versions don't support armv7 anymore _url=${GHCUP_BASE_URL}/0.1.40.0/armv7-linux-ghcup-0.1.40.0 elif [ "$(getconf LONG_BIT)" = "64" ] ; then _url=${GHCUP_BASE_URL}/${ghver}/aarch64-linux-ghcup-${ghver} else die "Unknown long bit size: $(getconf LONG_BIT)" fi ;; *) die "Unknown architecture: ${arch}" ;; esac ;; "FreeBSD"|"freebsd") case "${arch}" in x86_64|amd64) ;; i*86) die "i386 currently not supported!" ;; *) die "Unknown architecture: ${arch}" ;; esac _url=${GHCUP_BASE_URL}/${ghver}/x86_64-portbld-freebsd-ghcup-${ghver} ;; "Darwin"|"darwin") case "${arch}" in x86_64|amd64) _url=${GHCUP_BASE_URL}/${ghver}/x86_64-apple-darwin-ghcup-${ghver} ;; aarch64|arm64|armv8l) _url=${GHCUP_BASE_URL}/${ghver}/aarch64-apple-darwin-ghcup-${ghver} ;; i*86) die "i386 currently not supported!" ;; *) die "Unknown architecture: ${arch}" ;; esac ;; MSYS*|MINGW*|CYGWIN*) case "${arch}" in x86_64|amd64) _url=${GHCUP_BASE_URL}/${ghver}/x86_64-mingw64-ghcup-${ghver}.exe ;; *) die "Unknown architecture: ${arch}" ;; esac ;; *) die "Unknown platform: ${plat}" ;; esac case "${plat}" in MSYS*|MINGW*|CYGWIN*) case "${BOOTSTRAP_HASKELL_DOWNLOADER}" in "curl") # shellcheck disable=SC2086 edo curl -Lf ${GHCUP_CURL_OPTS} "${_url}" > "${GHCUP_BIN}"/ghcup.exe ;; "wget") # shellcheck disable=SC2086 edo wget -O /dev/stdout ${GHCUP_WGET_OPTS} "${_url}" > "${GHCUP_BIN}"/ghcup.exe ;; *) die "Unknown downloader: ${BOOTSTRAP_HASKELL_DOWNLOADER}" ;; esac edo chmod +x "${GHCUP_BIN}"/ghcup.exe ;; *) case "${BOOTSTRAP_HASKELL_DOWNLOADER}" in "curl") # shellcheck disable=SC2086 edo curl -Lf ${GHCUP_CURL_OPTS} "${_url}" > "${GHCUP_BIN}"/ghcup ;; "wget") # shellcheck disable=SC2086 edo wget -O /dev/stdout ${GHCUP_WGET_OPTS} "${_url}" > "${GHCUP_BIN}"/ghcup ;; *) die "Unknown downloader: ${BOOTSTRAP_HASKELL_DOWNLOADER}" ;; esac edo chmod +x "${GHCUP_BIN}"/ghcup ;; esac edo mkdir -p "${GHCUP_DIR}" # we may overwrite this in adjust_bashrc cat <<-EOF > "${GHCUP_DIR}"/env || die "Failed to create env file" case ":\$PATH:" in *:"${GHCUP_BIN}":*) ;; *) export PATH="${GHCUP_BIN}:\$PATH" ;; esac case ":\$PATH:" in *:"\$HOME/.cabal/bin":*) ;; *) export PATH="\$HOME/.cabal/bin:\$PATH" ;; esac EOF # shellcheck disable=SC1090 edo . "${GHCUP_DIR}"/env case "${BOOTSTRAP_HASKELL_DOWNLOADER}" in "curl") eghcup_raw config set downloader Curl ;; "wget") eghcup_raw config set downloader Wget ;; *) die "Unknown downloader: ${BOOTSTRAP_HASKELL_DOWNLOADER}" ;; esac eghcup upgrade } # Figures out the users login shell and sets # GHCUP_PROFILE_FILE and MY_SHELL variables. find_shell() { case $SHELL in */zsh) # login shell is zsh if [ -n "$ZDOTDIR" ]; then GHCUP_PROFILE_FILE="$ZDOTDIR/.zshrc" else GHCUP_PROFILE_FILE="$HOME/.zshrc" fi MY_SHELL="zsh" ;; */bash) # login shell is bash GHCUP_PROFILE_FILE="$HOME/.bashrc" MY_SHELL="bash" ;; */sh) # login shell is sh, but might be a symlink to bash or zsh if [ -n "${BASH}" ] ; then GHCUP_PROFILE_FILE="$HOME/.bashrc" MY_SHELL="bash" elif [ -n "${ZSH_VERSION}" ] ; then GHCUP_PROFILE_FILE="$HOME/.zshrc" MY_SHELL="zsh" else return fi ;; */fish) # login shell is fish GHCUP_PROFILE_FILE="$HOME/.config/fish/config.fish" MY_SHELL="fish" ;; *) return ;; esac } ask_base_channel() { if [ -n "${BOOTSTRAP_HASKELL_NONINTERACTIVE}" ] ; then return 0 fi echo "-------------------------------------------------------------------------------" warn "" warn "GHCup provides different binary distribution \"channels\". These are collections of tools" warn "and may differ in purpose and philosophy. First, we select the base channel." warn "" while true; do warn "[S] Skip [D] Default (GHCup maintained) [V] Vanilla (Upstream maintained) [?] Help (default is \"Skip\")." warn "" read -r bashrc_answer "${GHCUP_DIR}"/env || die "Failed to create env file" case ":\$PATH:" in *:"${GHCUP_BIN}":*) ;; *) export PATH="${GHCUP_BIN}:\$PATH" ;; esac case ":\$PATH:" in *:"\$HOME/.cabal/bin":*) ;; *) export PATH="\$HOME/.cabal/bin:\$PATH" ;; esac EOF ;; 2) cat <<-EOF > "${GHCUP_DIR}"/env || die "Failed to create env file" case ":\$PATH:" in *:"\$HOME/.cabal/bin":*) ;; *) export PATH="\$PATH:\$HOME/.cabal/bin" ;; esac case ":\$PATH:" in *:"${GHCUP_BIN}":*) ;; *) export PATH="\$PATH:${GHCUP_BIN}" ;; esac EOF ;; *) ;; esac case $1 in 1 | 2) case $MY_SHELL in "") warn_path "Couldn't figure out login shell!" return ;; fish) mkdir -p "${GHCUP_PROFILE_FILE%/*}" sed -i -e '/# ghcup-env$/d' "$(posix_realpath "${GHCUP_PROFILE_FILE}")" case $1 in 1) printf "\n%s" "set -q GHCUP_INSTALL_BASE_PREFIX[1]; or set GHCUP_INSTALL_BASE_PREFIX \$HOME ; set -gx PATH \$HOME/.cabal/bin $GHCUP_BIN \$PATH # ghcup-env" >> "${GHCUP_PROFILE_FILE}" ;; 2) printf "\n%s" "set -q GHCUP_INSTALL_BASE_PREFIX[1]; or set GHCUP_INSTALL_BASE_PREFIX \$HOME ; set -gx PATH \$HOME/.cabal/bin \$PATH $GHCUP_BIN # ghcup-env" >> "${GHCUP_PROFILE_FILE}" ;; esac ;; bash) sed -i -e '/# ghcup-env$/d' "$(posix_realpath "${GHCUP_PROFILE_FILE}")" printf "\n%s" "[ -f \"${GHCUP_DIR}/env\" ] && . \"${GHCUP_DIR}/env\" # ghcup-env" >> "${GHCUP_PROFILE_FILE}" case "${plat}" in "Darwin"|"darwin") if ! grep -q "ghcup-env" "${HOME}/.bash_profile" ; then printf "\n%s" "[[ -f ~/.bashrc ]] && . ~/.bashrc # ghcup-env" >> "${HOME}/.bash_profile" fi ;; MSYS*|MINGW*|CYGWIN*) if [ ! -e "${HOME}/.bash_profile" ] ; then echo '# generated by ghcup' > "${HOME}/.bash_profile" echo 'test -f ~/.profile && . ~/.profile' >> "${HOME}/.bash_profile" echo 'test -f ~/.bashrc && . ~/.bashrc' >> "${HOME}/.bash_profile" fi ;; esac ;; zsh) sed -i -e '/# ghcup-env$/d' "$(posix_realpath "${GHCUP_PROFILE_FILE}")" printf "\n%s" "[ -f \"${GHCUP_DIR}/env\" ] && . \"${GHCUP_DIR}/env\" # ghcup-env" >> "${GHCUP_PROFILE_FILE}" ;; esac if [ -e "$HOME/.profile" ] ; then sed -i -e '/# ghcup-env$/d' "$(posix_realpath "$HOME/.profile")" printf "\n%s" "[ -f \"${GHCUP_DIR}/env\" ] && . \"${GHCUP_DIR}/env\" # ghcup-env" >> "$HOME/.profile" fi echo echo "===============================================================================" echo warn "OK! ${GHCUP_PROFILE_FILE} has been modified. Restart your terminal for the changes to take effect," warn "or type \". ${GHCUP_DIR}/env\" to apply them in your current terminal session." return ;; *) warn_path ;; esac } warn_path() { echo echo "===============================================================================" echo [ -n "$1" ] && warn "$1" yellow "In order to run ghc and cabal, you need to adjust your PATH variable." yellow "To do so, you may want to run 'source $GHCUP_DIR/env' in your current terminal" yellow "session as well as your shell configuration (e.g. ~/.bashrc)." } adjust_cabal_config() { if [ -n "${CABAL_DIR}" ] ; then cabal_bin="${CABAL_DIR}/bin" else cabal_bin="$HOME/AppData/Roaming/cabal/bin" fi ecabal user-config -a "extra-prog-path: $(cygpath -w "$GHCUP_BIN"), $(cygpath -w "$cabal_bin"), $(cygpath -w "$GHCUP_MSYS2/${GHCUP_MSYS2_ENV_DIR}/bin"), $(cygpath -w "$GHCUP_MSYS2"/usr/bin)" -a "extra-include-dirs: $(cygpath -w "$GHCUP_MSYS2/${GHCUP_MSYS2_ENV_DIR}/include")" -a "extra-lib-dirs: $(cygpath -w "$GHCUP_MSYS2/${GHCUP_MSYS2_ENV_DIR}/lib")" -f init } ask_cabal_config_init() { case "${plat}" in MSYS*|MINGW*|CYGWIN*) if [ -n "${BOOTSTRAP_HASKELL_ADJUST_CABAL_CONFIG}" ] ; then return 1 fi if [ -z "${BOOTSTRAP_HASKELL_NONINTERACTIVE}" ] ; then echo "-------------------------------------------------------------------------------" warn "Create an initial cabal.config including relevant msys2 paths (recommended)?" warn "[Y] Yes [N] No [?] Help (default is \"Y\")." echo while true; do read -r mingw_answer /dev/null 2>&1 ; then if [ -z "${BOOTSTRAP_HASKELL_NO_UPGRADE}" ] ; then ( _eghcup upgrade ) || download_ghcup fi else download_ghcup fi echo if [ -n "${BOOTSTRAP_HASKELL_YAML}" ] ; then (>&2 ghcup -s "${BOOTSTRAP_HASKELL_YAML}" tool-requirements) ; else (>&2 ghcup tool-requirements) ; fi echo if [ -z "${BOOTSTRAP_HASKELL_NONINTERACTIVE}" ] ; then echo warn "Press ENTER to proceed or ctrl-c to abort." echo # Wait for user input to continue. # shellcheck disable=SC2034 read -r answer "${hook_exe}" ;; "wget") # shellcheck disable=SC2086 edo wget -O /dev/stdout ${GHCUP_WGET_OPTS} "${hook_url}" > "${hook_exe}" ;; *) die "Unknown downloader: ${BOOTSTRAP_HASKELL_DOWNLOADER}" ;; esac edo chmod +x "${hook_exe}" fi ;; *) ;; esac adjust_bashrc $ask_bashrc_answer _done ) # vim: tabstop=4 shiftwidth=4 expandtab