├── .gitignore ├── halcyon ├── LICENSE ├── README.md ├── src.sh ├── src ├── tag.sh ├── help.sh ├── paths.sh ├── storage.sh ├── constraints.sh ├── install.sh ├── build.sh ├── sandbox.sh ├── main.sh ├── core.sh ├── cabal.sh └── ghc.sh ├── setup.sh └── bin └── test /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /halcyon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | export HALCYON_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd -P ) 6 | 7 | source "${HALCYON_DIR}/src.sh" 8 | 9 | set_halcyon_vars 10 | halcyon_main "$@" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2014-2015, Mietek Bak 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | 3 | This project is no longer maintained. 4 | 5 | ------------------------------------------------------------------------------- 6 | 7 | 8 | [Halcyon](https://halcyon.sh/) 9 | ============================== 10 | 11 | Halcyon is a system for installing [Haskell](https://haskell.org/) apps and development tools, including [GHC](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/) and [Cabal](https://www.haskell.org/cabal/users-guide/). 12 | 13 | **Follow the [Halcyon tutorial](https://halcyon.sh/tutorial/) to get started.** 14 | 15 | 16 | Usage 17 | ----- 18 | 19 | Halcyon provides the [`halcyon install`](https://halcyon.sh/reference/#halcyon-install) command, which can be used to install Haskell apps: 20 | 21 | ``` 22 | $ halcyon install https://github.com/mietek/halcyon-tutorial 23 | ``` 24 | 25 | 26 | ### Installation 27 | 28 | Halcyon can be installed by cloning the [Halcyon source repository](https://github.com/mietek/halcyon): 29 | 30 | ``` 31 | $ git clone https://github.com/mietek/halcyon 32 | ``` 33 | 34 | Alternatively, you can run the [Halcyon setup script](https://github.com/mietek/halcyon/blob/master/setup.sh), which also installs the necessary OS packages and sets up the environment: 35 | 36 | ``` 37 | $ eval "$( curl -sL https://github.com/mietek/halcyon/raw/master/setup.sh )" 38 | ``` 39 | 40 | 41 | ### Documentation 42 | 43 | - **Start with the [Halcyon tutorial](https://halcyon.sh/tutorial/) to learn how to deploy a simple Haskell web app using Halcyon.** 44 | 45 | - See the [Halcyon reference](https://halcyon.sh/reference/) for a complete list of available commands and options. 46 | 47 | - Read the [Haskell on Heroku tutorial](https://haskellonheroku.com/tutorial/) to learn how to deploy Haskell web apps to [Heroku](https://heroku.com/). 48 | 49 | 50 | #### Internals 51 | 52 | Halcyon is written in [GNU _bash_](https://gnu.org/software/bash/), using the [_bashmenot_](https://bashmenot.mietek.io/) library. 53 | 54 | - Read the [Halcyon source code](https://github.com/mietek/halcyon) to understand how it works. 55 | 56 | 57 | About 58 | ----- 59 | 60 | Made by [Miëtek Bak](https://mietek.io/). Published under the BSD license. 61 | -------------------------------------------------------------------------------- /src.sh: -------------------------------------------------------------------------------- 1 | unset POSIXLY_CORRECT 2 | 3 | set -o pipefail 4 | 5 | export HALCYON_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd -P ) 6 | 7 | 8 | install_bashmenot () { 9 | local no_update 10 | no_update="${HALCYON_NO_SELF_UPDATE:-0}" 11 | if (( ${HALCYON_INTERNAL_RECURSIVE:-0} )); then 12 | no_update=1 13 | fi 14 | 15 | if [[ -d "${HALCYON_DIR}/lib/bashmenot" ]]; then 16 | BASHMENOT_NO_SELF_UPDATE="${no_update}" \ 17 | source "${HALCYON_DIR}/lib/bashmenot/src.sh" || return 1 18 | return 0 19 | fi 20 | 21 | local url base_url branch 22 | url="${BASHMENOT_URL:-https://github.com/mietek/bashmenot}" 23 | base_url="${url%#*}" 24 | branch="${url#*#}" 25 | if [[ "${branch}" == "${base_url}" ]]; then 26 | branch='master' 27 | fi 28 | 29 | printf -- '-----> Installing bashmenot...' >&2 30 | 31 | local commit_hash 32 | if ! commit_hash=$( 33 | git clone -q "${base_url}" "${HALCYON_DIR}/lib/bashmenot" >'/dev/null' 2>&1 && 34 | cd "${HALCYON_DIR}/lib/bashmenot" && 35 | git checkout -q "${branch}" >'/dev/null' 2>&1 && 36 | git log -n 1 --pretty='format:%h' 37 | ); then 38 | echo ' error' >&2 39 | return 1 40 | fi 41 | echo " done, ${commit_hash}" >&2 42 | 43 | BASHMENOT_NO_SELF_UPDATE=1 \ 44 | source "${HALCYON_DIR}/lib/bashmenot/src.sh" || return 1 45 | } 46 | 47 | 48 | if ! install_bashmenot; then 49 | echo ' *** ERROR: Failed to install bashmenot' >&2 50 | exit 1 51 | fi 52 | 53 | export HALCYON_INTERNAL_PLATFORM=$( detect_platform ) 54 | 55 | source "${HALCYON_DIR}/src/paths.sh" 56 | source "${HALCYON_DIR}/src/main.sh" 57 | source "${HALCYON_DIR}/src/tag.sh" 58 | source "${HALCYON_DIR}/src/core.sh" 59 | source "${HALCYON_DIR}/src/storage.sh" 60 | source "${HALCYON_DIR}/src/constraints.sh" 61 | source "${HALCYON_DIR}/src/ghc.sh" 62 | source "${HALCYON_DIR}/src/cabal.sh" 63 | source "${HALCYON_DIR}/src/sandbox.sh" 64 | source "${HALCYON_DIR}/src/build.sh" 65 | source "${HALCYON_DIR}/src/install.sh" 66 | source "${HALCYON_DIR}/src/help.sh" 67 | 68 | 69 | halcyon_self_update () { 70 | if (( ${HALCYON_NO_SELF_UPDATE:-0} )) || 71 | (( ${HALCYON_INTERNAL_RECURSIVE:-0} )) || 72 | [[ ! -d "${HALCYON_DIR}/.git" ]] 73 | then 74 | return 0 75 | fi 76 | 77 | local now candidate_time 78 | now=$( get_current_time ) 79 | if candidate_time=$( get_modification_time "${HALCYON_DIR}" ) && 80 | (( candidate_time + 60 >= now )) 81 | then 82 | return 0 83 | fi 84 | 85 | local url 86 | url="${HALCYON_URL:-https://github.com/mietek/halcyon}" 87 | 88 | log_begin 'Self-updating Halcyon...' 89 | 90 | local commit_hash 91 | if ! commit_hash=$( git_update_into "${url}" "${HALCYON_DIR}" ); then 92 | log_end 'error' 93 | return 0 94 | fi 95 | log_end "done, ${commit_hash}" 96 | 97 | touch "${HALCYON_DIR}" || return 1 98 | 99 | HALCYON_NO_SELF_UPDATE=1 \ 100 | source "${HALCYON_DIR}/src.sh" 101 | } 102 | 103 | 104 | if ! halcyon_self_update; then 105 | log_error 'Failed to self-update Halcyon' 106 | exit 1 107 | fi 108 | -------------------------------------------------------------------------------- /src/tag.sh: -------------------------------------------------------------------------------- 1 | create_tag () { 2 | expect_vars HALCYON_BASE 3 | 4 | local prefix label source_hash constraints_hash magic_hash \ 5 | ghc_version ghc_magic_hash \ 6 | cabal_version cabal_magic_hash cabal_remote_repo cabal_date \ 7 | sandbox_magic_hash 8 | expect_args prefix label source_hash constraints_hash magic_hash \ 9 | ghc_version ghc_magic_hash \ 10 | cabal_version cabal_magic_hash cabal_remote_repo cabal_date \ 11 | sandbox_magic_hash -- "$@" 12 | 13 | echo -e "1\t${HALCYON_INTERNAL_PLATFORM}\t${HALCYON_BASE}\t${prefix}\t${label}\t${source_hash}\t${constraints_hash}\t${magic_hash}\t${ghc_version}\t${ghc_magic_hash}\t${cabal_version}\t${cabal_magic_hash}\t${cabal_remote_repo}\t${cabal_date}\t${sandbox_magic_hash}" 14 | } 15 | 16 | 17 | get_tag_version () { 18 | local tag 19 | expect_args tag -- "$@" 20 | 21 | awk -F$'\t' '{ print $1 }' <<<"${tag}" || return 0 22 | } 23 | 24 | 25 | get_tag_platform () { 26 | local tag 27 | expect_args tag -- "$@" 28 | 29 | awk -F$'\t' '{ print $2 }' <<<"${tag}" || return 0 30 | } 31 | 32 | 33 | get_tag_base () { 34 | local tag 35 | expect_args tag -- "$@" 36 | 37 | awk -F$'\t' '{ print $3 }' <<<"${tag}" || return 0 38 | } 39 | 40 | 41 | get_tag_prefix () { 42 | local tag 43 | expect_args tag -- "$@" 44 | 45 | awk -F$'\t' '{ print $4 }' <<<"${tag}" || return 0 46 | } 47 | 48 | 49 | get_tag_label () { 50 | local tag 51 | expect_args tag -- "$@" 52 | 53 | awk -F$'\t' '{ print $5 }' <<<"${tag}" || return 0 54 | } 55 | 56 | 57 | get_tag_source_hash () { 58 | local tag 59 | expect_args tag -- "$@" 60 | 61 | awk -F$'\t' '{ print $6 }' <<<"${tag}" || return 0 62 | } 63 | 64 | 65 | get_tag_constraints_hash () { 66 | local tag 67 | expect_args tag -- "$@" 68 | 69 | awk -F$'\t' '{ print $7 }' <<<"${tag}" || return 0 70 | } 71 | 72 | 73 | get_tag_magic_hash () { 74 | local tag 75 | expect_args tag -- "$@" 76 | 77 | awk -F$'\t' '{ print $8 }' <<<"${tag}" || return 0 78 | } 79 | 80 | 81 | get_tag_ghc_version () { 82 | local tag 83 | expect_args tag -- "$@" 84 | 85 | awk -F$'\t' '{ print $9 }' <<<"${tag}" | sed 's/^ghc-//' || return 0 86 | } 87 | 88 | 89 | get_tag_ghc_magic_hash () { 90 | local tag 91 | expect_args tag -- "$@" 92 | 93 | awk -F$'\t' '{ print $10 }' <<<"${tag}" || return 0 94 | } 95 | 96 | 97 | get_tag_cabal_version () { 98 | local tag 99 | expect_args tag -- "$@" 100 | 101 | awk -F$'\t' '{ print $11 }' <<<"${tag}" | sed 's/^cabal-//' || return 0 102 | } 103 | 104 | 105 | get_tag_cabal_magic_hash () { 106 | local tag 107 | expect_args tag -- "$@" 108 | 109 | awk -F$'\t' '{ print $12 }' <<<"${tag}" || return 0 110 | } 111 | 112 | 113 | get_tag_cabal_remote_repo () { 114 | local tag 115 | expect_args tag -- "$@" 116 | 117 | awk -F$'\t' '{ print $13 }' <<<"${tag}" || return 0 118 | } 119 | 120 | 121 | get_tag_cabal_date () { 122 | local tag 123 | expect_args tag -- "$@" 124 | 125 | awk -F$'\t' '{ print $14 }' <<<"${tag}" || return 0 126 | } 127 | 128 | 129 | get_tag_sandbox_magic_hash () { 130 | local tag 131 | expect_args tag -- "$@" 132 | 133 | awk -F$'\t' '{ print $15 }' <<<"${tag}" || return 0 134 | } 135 | 136 | 137 | detect_tag () { 138 | local file tag_pattern 139 | expect_args file tag_pattern -- "$@" 140 | 141 | if [[ ! -f "${file}" ]]; then 142 | return 1 143 | fi 144 | 145 | local candidate_tag 146 | candidate_tag=$( 147 | filter_matching "^${tag_pattern}$" <"${file}" | 148 | match_exactly_one 149 | ) || return 1 150 | 151 | echo "${candidate_tag}" 152 | } 153 | -------------------------------------------------------------------------------- /src/help.sh: -------------------------------------------------------------------------------- 1 | help_usage () { 2 | cat <<-EOF 3 | Usage: 4 | halcyon COMMAND 5 | 6 | Commands: 7 | install APP? OPTION* 8 | build APP? OPTION* 9 | label APP? OPTION* 10 | executable APP? OPTION* 11 | constraints APP? OPTION* 12 | paths 13 | 14 | App: 15 | label 16 | directory path 17 | git URL 18 | nothing 19 | 20 | General options: 21 | --base= DIR 22 | --prefix= DIR 23 | --root= DIR 24 | --no-app 25 | --no-modify-home 26 | --log-timestamp 27 | 28 | Build-time options: 29 | --constraints= STRING | FILE | DIR 30 | --extra-source-hash-ignore= STRING | FILE 31 | --extra-configure-flags= STRING | FILE 32 | --pre-build-hook= FILE 33 | --post-build-hook= FILE 34 | --app-rebuild 35 | --app-reconfigure 36 | --app-no-strip 37 | --ignore-all-constraints 38 | --no-build 39 | --no-build-dependencies 40 | --dependencies-only 41 | 42 | Install-time options: 43 | --extra-apps= STRING | FILE 44 | --extra-apps-constraints= STRING | FILE | DIR 45 | --extra-data-files= STRING | FILE 46 | --extra-os-packages= STRING | FILE 47 | --pre-install-hook= FILE 48 | --post-install-hook= FILE 49 | --app-reinstall 50 | --app-no-remove-doc 51 | --keep-dependencies 52 | 53 | Cache options: 54 | --cache= DIR 55 | --purge-cache 56 | --no-archive 57 | --no-clean-cache 58 | 59 | Public storage options: 60 | --public-storage-url= S3_URL 61 | --no-public-storage 62 | 63 | Private storage options: 64 | --aws-access-key-id= STRING 65 | --aws-secret-access-key= STRING 66 | --s3-bucket= S3_NAME 67 | --s3-endpoint= S3_ADDRESS 68 | --s3-acl= S3_ACL 69 | --no-private-storage 70 | --no-upload 71 | --no-clean-private-storage 72 | 73 | GHC options: 74 | --ghc-version= VERSION 75 | --ghc-pre-build-hook= FILE 76 | --ghc-post-build-hook= FILE 77 | --ghc-rebuild 78 | --ghc-no-remove-doc 79 | --ghc-no-strip 80 | 81 | Cabal options: 82 | --cabal-version= VERSION 83 | --cabal-remote-repo= STRING | FILE 84 | --cabal-pre-build-hook= FILE 85 | --cabal-post-build-hook= FILE 86 | --cabal-pre-update-hook= FILE 87 | --cabal-post-update-hook= FILE 88 | --cabal-rebuild 89 | --cabal-update 90 | --cabal-no-strip 91 | --cabal-no-update 92 | 93 | Sandbox options: 94 | --sandbox-extra-configure-flags= STRING | FILE 95 | --sandbox-sources= STRING | FILE 96 | --sandbox-extra-apps= STRING | FILE 97 | --sandbox-extra-apps-constraints= STRING | FILE | DIR 98 | --sandbox-extra-os-packages= STRING | FILE 99 | --sandbox-pre-build-hook= FILE 100 | --sandbox-post-build-hook= FILE 101 | --sandbox-rebuild 102 | --sandbox-no-remove-doc 103 | --sandbox-no-strip 104 | EOF 105 | } 106 | -------------------------------------------------------------------------------- /src/paths.sh: -------------------------------------------------------------------------------- 1 | if [ "${HALCYON_INTERNAL_PATHS:-0}" -eq 0 ]; then 2 | export HALCYON_INTERNAL_PATHS=1 3 | 4 | export HALCYON_BASE="${HALCYON_BASE:-/app}" 5 | 6 | export CABAL_CONFIG="${HALCYON_BASE}/cabal/config" 7 | 8 | _join () { 9 | IFS=':' && echo "$*" 10 | } 11 | 12 | _path=$( _join \ 13 | "${HALCYON_DIR}" \ 14 | "${HALCYON_BASE}/bin" \ 15 | "${HALCYON_BASE}/usr/bin" \ 16 | "${HALCYON_BASE}/ghc/bin" \ 17 | "${HALCYON_BASE}/cabal/bin" \ 18 | "${HALCYON_BASE}/sandbox/bin" \ 19 | "${HALCYON_BASE}/sandbox/usr/bin" 20 | ) 21 | export PATH="${_path}:${PATH:-}" 22 | 23 | _path=$( _join \ 24 | "${HALCYON_BASE}/include" \ 25 | "${HALCYON_BASE}/usr/include" \ 26 | "${HALCYON_BASE}/sandbox/include" \ 27 | "${HALCYON_BASE}/sandbox/usr/include" 28 | ) 29 | case "${HALCYON_INTERNAL_PLATFORM}" in 30 | 'linux-debian-'*'-i386'|'linux-ubuntu-'*'-i386') 31 | _path=$( _join "${_path}" \ 32 | "${HALCYON_BASE}/include/i386-linux-gnu" \ 33 | "${HALCYON_BASE}/usr/include/i386-linux-gnu" \ 34 | "${HALCYON_BASE}/sandbox/include/i386-linux-gnu" \ 35 | "${HALCYON_BASE}/sandbox/usr/include/i386-linux-gnu" 36 | ) 37 | ;; 38 | 'linux-debian-'*'-x86_64'|'linux-ubuntu-'*'-x86_64') 39 | _path=$( _join "${_path}" \ 40 | "${HALCYON_BASE}/include/x86_64-linux-gnu" \ 41 | "${HALCYON_BASE}/usr/include/x86_64-linux-gnu" \ 42 | "${HALCYON_BASE}/sandbox/include/x86_64-linux-gnu" \ 43 | "${HALCYON_BASE}/sandbox/usr/include/x86_64-linux-gnu" 44 | ) 45 | esac 46 | export CPATH="${_path}:${CPATH:-}" 47 | 48 | _path=$( _join \ 49 | "${HALCYON_BASE}/lib" \ 50 | "${HALCYON_BASE}/usr/lib" \ 51 | "${HALCYON_BASE}/ghc/usr/lib" \ 52 | "${HALCYON_BASE}/sandbox/lib" \ 53 | "${HALCYON_BASE}/sandbox/usr/lib" 54 | ) 55 | case "${HALCYON_INTERNAL_PLATFORM}" in 56 | 'linux-amzn-'*|'linux-centos-'*|'linux-fedora-'*|'linux-rhel-'*) 57 | _path=$( _join "${_path}" \ 58 | "${HALCYON_BASE}/lib64" \ 59 | "${HALCYON_BASE}/usr/lib64" \ 60 | "${HALCYON_BASE}/sandbox/lib64" \ 61 | "${HALCYON_BASE}/sandbox/usr/lib64" 62 | ) 63 | ;; 64 | 'linux-debian-'*'-i386'|'linux-ubuntu-'*'-i386') 65 | _path=$( _join "${_path}" \ 66 | "${HALCYON_BASE}/lib/i386-linux-gnu" \ 67 | "${HALCYON_BASE}/usr/lib/i386-linux-gnu" \ 68 | "${HALCYON_BASE}/sandbox/lib/i386-linux-gnu" \ 69 | "${HALCYON_BASE}/sandbox/usr/lib/i386-linux-gnu" 70 | ) 71 | ;; 72 | 'linux-debian-'*'-x86_64'|'linux-ubuntu-'*'-x86_64') 73 | _path=$( _join "${_path}" \ 74 | "${HALCYON_BASE}/lib/x86_64-linux-gnu" \ 75 | "${HALCYON_BASE}/usr/lib/x86_64-linux-gnu" \ 76 | "${HALCYON_BASE}/sandbox/lib/x86_64-linux-gnu" \ 77 | "${HALCYON_BASE}/sandbox/usr/lib/x86_64-linux-gnu" 78 | ) 79 | esac 80 | export LIBRARY_PATH="${_path}:${LIBRARY_PATH:-}" 81 | export LD_LIBRARY_PATH="${_path}:${LD_LIBRARY_PATH:-}" 82 | 83 | _path=$( _join \ 84 | "${HALCYON_BASE}/usr/lib/pkgconfig" \ 85 | "${HALCYON_BASE}/usr/share/pkgconfig" \ 86 | "${HALCYON_BASE}/sandbox/usr/lib/pkgconfig" \ 87 | "${HALCYON_BASE}/sandbox/usr/share/pkgconfig" 88 | ) 89 | case "${HALCYON_INTERNAL_PLATFORM}" in 90 | 'linux-amzn-'*|'linux-centos-'*|'linux-fedora-'*|'linux-rhel-'*) 91 | _path=$( _join "${_path}" \ 92 | "${HALCYON_BASE}/usr/lib64/pkgconfig" \ 93 | "${HALCYON_BASE}/sandbox/usr/lib64/pkgconfig" 94 | ) 95 | ;; 96 | 'linux-debian-'*'-i386'|'linux-ubuntu-'*'-i386') 97 | _path=$( _join "${_path}" \ 98 | "${HALCYON_BASE}/usr/lib/i386-linux-gnu/pkgconfig" \ 99 | "${HALCYON_BASE}/sandbox/usr/lib/i386-linux-gnu/pkgconfig" 100 | ) 101 | ;; 102 | 'linux-debian-'*'-x86_64'|'linux-ubuntu-'*'-x86_64') 103 | _path=$( _join "${_path}" \ 104 | "${HALCYON_BASE}/usr/lib/x86_64-linux-gnu/pkgconfig" \ 105 | "${HALCYON_BASE}/sandbox/usr/lib/x86_64-linux-gnu/pkgconfig" 106 | ) 107 | esac 108 | export PKG_CONFIG_PATH="${_path}:${PKG_CONFIG_PATH:-}" 109 | 110 | unset _join _path 111 | 112 | # NOTE: UTF-8 locale is needed to work around a Cabal issue. 113 | # https://github.com/haskell/cabal/issues/1883 114 | case "${HALCYON_INTERNAL_PLATFORM}" in 115 | 'freebsd-'*) 116 | export LANG="${LANG:-en_US.UTF-8}" 117 | ;; 118 | *) 119 | export LANG="${LANG:-C.UTF-8}" 120 | esac 121 | fi 122 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | set -o pipefail 2 | 3 | 4 | install_os_packages () { 5 | local platform uid 6 | platform="$1" 7 | uid="$2" 8 | 9 | case "${platform}" in 10 | 'linux-amzn-2014.09-'*) 11 | sudo bash -c "yum groupinstall -y 'Development Tools' && 12 | yum install -y git pigz zlib-devel" || return 1 13 | ;; 14 | 'linux-arch'*) 15 | # NOTE: There is no sudo on Arch Linux. 16 | if [ "${uid}" -eq 0 ]; then 17 | pacman --sync --needed --noconfirm base-devel git pigz zlib || return 1 18 | else 19 | echo ' *** WARNING: Cannot install OS packages' >&2 20 | echo ' *** WARNING: Ensure the following OS packages are installed:' >&2 21 | echo ' $ pacman --sync --needed --noconfirm base-devel git pigz zlib' >&2 22 | fi 23 | ;; 24 | 'linux-centos-6'*) 25 | sudo bash -c "yum groupinstall -y 'Development Tools' && 26 | yum install -y git yum-plugin-downloadonly zlib-devel" || return 1 27 | ;; 28 | 'linux-centos-7'*) 29 | sudo bash -c "yum groupinstall -y 'Development Tools' && 30 | yum install -y git zlib-devel" || return 1 31 | ;; 32 | 'linux-debian-6'*) 33 | # NOTE: There is no sudo on Debian 6. 34 | if [ "${uid}" -eq 0 ]; then 35 | apt-get update || return 1 36 | apt-get install -y build-essential git pigz zlib1g-dev || return 1 37 | else 38 | echo ' *** WARNING: Cannot install OS packages' >&2 39 | echo ' *** WARNING: Ensure the following OS packages are installed:' >&2 40 | echo ' $ apt-get update' >&2 41 | echo ' $ apt-get install -y build-essential git pigz zlib1g-dev' >&2 42 | fi 43 | ;; 44 | 'linux-debian-7'*|'linux-debian-8'*) 45 | # NOTE: When run as root, sudo asks for password 46 | # on Debian 7. 47 | if [ "${uid}" -eq 0 ]; then 48 | apt-get update || return 1 49 | apt-get install -y build-essential git pigz zlib1g-dev || return 1 50 | else 51 | sudo bash -c 'apt-get update && 52 | apt-get install -y build-essential git pigz zlib1g-dev' || return 1 53 | fi 54 | ;; 55 | 'linux-exherbo'*) 56 | # NOTE: There is no sudo on Exherbo Linux. 57 | if [ "${uid}" -eq 0 ]; then 58 | cave resolve -x app-arch/pigz sys-libs/zlib || return 1 59 | else 60 | echo ' *** WARNING: Cannot install OS packages' >&2 61 | echo ' *** WARNING: Ensure the following OS packages are installed:' >&2 62 | echo ' $ cave resolve -x app-arch/pigz sys-libs/zlib' >&2 63 | fi 64 | ;; 65 | 'linux-fedora-19'*) 66 | sudo bash -c "yum groupinstall -y 'Development Tools' && 67 | yum install -y git pigz zlib-devel" || return 1 68 | ;; 69 | 'linux-fedora-20'*) 70 | sudo bash -c "yum groupinstall -y 'Development Tools' && 71 | yum install -y git patch pigz tar zlib-devel" || return 1 72 | ;; 73 | 'linux-fedora-21'*) 74 | sudo bash -c "yum groupinstall -y 'Development Tools' && 75 | yum install -y git patch openssl pigz tar which zlib-devel" || return 1 76 | ;; 77 | 'linux-gentoo'*) 78 | # NOTE: There is no sudo on Gentoo Linux. 79 | if [ "${uid}" -eq 0 ]; then 80 | emerge --noreplace app-arch/pigz dev-vcs/git || return 1 81 | else 82 | echo ' *** WARNING: Cannot install OS packages' >&2 83 | echo ' *** WARNING: Ensure the following OS packages are installed:' >&2 84 | echo ' $ emerge --noreplace app-arch/pigz dev-vcs/git' >&2 85 | fi 86 | ;; 87 | 'linux-opensuse-13'*) 88 | sudo bash -c 'zypper refresh && 89 | zypper -n install -t pattern devel_basis && 90 | zypper -n install git' || return 1 91 | ;; 92 | 'linux-rhel-6'*'-x86_64') 93 | sudo bash -c "yum groupinstall -y 'Development Tools' && 94 | yum install -y git zlib-devel" || return 1 95 | ;; 96 | 'linux-rhel-7'*) 97 | sudo bash -c "yum groupinstall -y 'Development Tools' && 98 | yum install -y git zlib-devel" || return 1 99 | ;; 100 | 'linux-slackware-14'*) 101 | # NOTE: Nothing to install on Slackware. 102 | ;; 103 | 'linux-sles-11'*) 104 | # NOTE: When run as root, sudo asks for password 105 | # on SLES 11. 106 | if [ "${uid}" -eq 0 ]; then 107 | zypper refresh || return 1 108 | zypper -n install -t pattern Basis-Devel || return 1 109 | zypper -n install git zlib-devel || return 1 110 | else 111 | sudo bash -c 'zypper refresh && 112 | zypper -n install -t pattern Basis-Devel && 113 | zypper -n install git zlib-devel' || return 1 114 | fi 115 | ;; 116 | 'linux-sles-12'*) 117 | sudo bash -c 'zypper -n install -t pattern Basis-Devel && 118 | zypper -n install git pigz zlib-devel' || return 1 119 | ;; 120 | 'linux-ubuntu-10'*) 121 | # NOTE: When run as root, sudo asks for password 122 | # on Ubuntu 10. 123 | if [ "${uid}" -eq 0 ]; then 124 | apt-get update || return 1 125 | apt-get install -y build-essential git-core pigz zlib1g-dev || return 1 126 | apt-get install -y --reinstall ca-certificates || return 1 127 | else 128 | sudo bash -c 'apt-get update && 129 | apt-get install -y build-essential git-core pigz zlib1g-dev && 130 | apt-get install -y --reinstall ca-certificates' || return 1 131 | fi 132 | ;; 133 | 'linux-ubuntu-12'*) 134 | # NOTE: When run as root, sudo asks for password 135 | # on Ubuntu 12. 136 | if [ "${uid}" -eq 0 ]; then 137 | apt-get update || return 1 138 | apt-get install -y build-essential git libgmp3c2 pigz zlib1g-dev || return 1 139 | else 140 | sudo bash -c 'apt-get update && 141 | apt-get install -y build-essential git libgmp3c2 pigz zlib1g-dev' || return 1 142 | fi 143 | ;; 144 | 'linux-ubuntu-14'*|'linux-ubuntu-15'*) 145 | sudo bash -c 'apt-get update && 146 | apt-get install -y build-essential git pigz zlib1g-dev' || return 1 147 | ;; 148 | 'osx-10.7-'*|'osx-10.8-'*) 149 | echo ' *** WARNING: Cannot install OS packages' >&2 150 | echo ' *** WARNING: Ensure the following OS packages are installed:' >&2 151 | echo ' $ brew update' >&2 152 | echo ' $ brew install bash coreutils git pigz xz' >&2 153 | ;; 154 | 'osx-10.9-'*|'osx-10.10-'*) 155 | echo ' *** WARNING: Cannot install OS packages' >&2 156 | echo ' *** WARNING: Ensure the following OS packages are installed:' >&2 157 | echo ' $ brew update' >&2 158 | echo ' $ brew install bash coreutils git pigz' >&2 159 | ;; 160 | *) 161 | echo " *** ERROR: Unsupported platform: ${platform}" >&2 162 | return 1 163 | esac 164 | } 165 | 166 | 167 | install_halcyon () { 168 | local base dir 169 | base="${HALCYON_BASE:-/app}" 170 | dir="${HALCYON_DIR:-${base}/halcyon}" 171 | 172 | echo '-----> Welcome to Halcyon' >&2 173 | 174 | if [ -e "${base}" ]; then 175 | echo " *** ERROR: Unexpected existing ${base}" >&2 176 | return 1 177 | fi 178 | if [ -e "${dir}" ]; then 179 | echo " *** ERROR: Unexpected existing ${dir}" >&2 180 | return 1 181 | fi 182 | 183 | local status 184 | status=0 185 | curl 2>'/dev/null' || status="$?" 186 | if [ "${status}" -eq 127 ]; then 187 | echo ' *** ERROR: Expected curl' >&2 188 | echo ' *** ERROR: Ensure the curl OS package is installed' >&2 189 | return 1 190 | fi 191 | 192 | eval "$( curl -sL 'https://github.com/mietek/bashmenot/raw/master/src/platform.sh' )" || return 1 193 | 194 | local platform uid user group 195 | platform=$( detect_platform ) 196 | uid=$( id -u ) || return 1 197 | user=$( id -nu ) || return 1 198 | group=$( id -ng ) || return 1 199 | 200 | echo >&2 201 | echo "-----> Creating base directory: ${base}" >&2 202 | 203 | case "${platform}" in 204 | 'linux-arch'*|'linux-debian-6'*|'linux-exherbo'*|'linux-gentoo'*) 205 | # NOTE: There is no sudo on Arch Linux, Debian 6, 206 | # Exherbo Linux, and Gentoo Linux. 207 | if [ "${uid}" -eq 0 ]; then 208 | mkdir -p "${base}" || return 1 209 | chown "${user}:${group}" "${base}" || return 1 210 | else 211 | echo ' *** WARNING: Cannot create base directory' >&2 212 | echo " *** WARNING: Ensure ${base} is owned by ${user}:${group}:" >&2 213 | echo ' $ mkdir -p "'"${base}"'"' >&2 214 | echo ' $ chown ${user}:${group} "'"${base}"'"' >&2 215 | fi 216 | ;; 217 | 'linux-debian-7'*|'linux-debian-8'*|'linux-sles-11'*|'linux-ubuntu-10'*|'linux-ubuntu-12'*) 218 | # NOTE: When run as root, sudo asks for password 219 | # on Debian 7, SLES 11, Ubuntu 10, and Ubuntu 12. 220 | if [ "${uid}" -eq 0 ]; then 221 | mkdir -p "${base}" || return 1 222 | chown "${user}:${group}" "${base}" || return 1 223 | else 224 | sudo -k mkdir -p "${base}" || return 1 225 | sudo chown "${user}:${group}" "${base}" || return 1 226 | fi 227 | ;; 228 | *) 229 | sudo -k mkdir -p "${base}" || return 1 230 | sudo chown "${user}:${group}" "${base}" || return 1 231 | esac 232 | 233 | echo '-----> Installing OS packages' >&2 234 | 235 | if ! install_os_packages "${platform}" "${uid}" 2>&1 | sed 's/^/ /' >&2; then 236 | echo ' *** ERROR: Failed to install OS packages' >&2 237 | return 1 238 | fi 239 | 240 | local url base_url branch 241 | url="${HALCYON_URL:-https://github.com/mietek/halcyon}" 242 | base_url="${url%#*}" 243 | branch="${url#*#}" 244 | if [ "${branch}" = "${base_url}" ]; then 245 | branch='master' 246 | fi 247 | 248 | echo >&2 249 | printf -- '-----> Installing Halcyon...' >&2 250 | 251 | local commit_hash 252 | if ! commit_hash=$( 253 | git clone -q "${base_url}" "${dir}" >'/dev/null' 2>&1 && 254 | cd "${dir}" && 255 | git checkout -q "${branch}" >'/dev/null' 2>&1 && 256 | git log -n 1 --pretty='format:%h' 257 | ); then 258 | echo ' error' >&2 259 | return 1 260 | fi 261 | echo " done, ${commit_hash}" >&2 262 | 263 | eval "$( HALCYON_NO_SELF_UPDATE=1 "${dir}/halcyon" paths )" || return 1 264 | 265 | echo '-----> Extending .bash_profile' >&2 266 | 267 | if [ "${base}" = '/app' ]; then 268 | echo 'eval "$( HALCYON_NO_SELF_UPDATE=1 "'"${dir}/halcyon"'" paths )"' >>"${HOME}/.bash_profile" || return 1 269 | else 270 | echo 'eval "$( HALCYON_NO_SELF_UPDATE=1 "'"${dir}/halcyon"'" --base="'"${base}"'" paths )"' >>"${HOME}/.bash_profile" || return 1 271 | fi 272 | 273 | echo >&2 274 | echo '-----> Halcyon installed' 275 | } 276 | 277 | 278 | if ! install_halcyon; then 279 | echo ' *** ERROR: Failed to install Halcyon' >&2 280 | fi 281 | -------------------------------------------------------------------------------- /src/storage.sh: -------------------------------------------------------------------------------- 1 | has_public_storage () { 2 | expect_vars HALCYON_BASE HALCYON_NO_PUBLIC_STORAGE 3 | 4 | # NOTE: All archives in public storage assume HALCYON_BASE is /app. 5 | [[ "${HALCYON_BASE}" == '/app' ]] || return 1 6 | 7 | ! (( HALCYON_NO_PUBLIC_STORAGE )) || return 1 8 | [[ -n "${HALCYON_PUBLIC_STORAGE_URL}" ]] || return 1 9 | } 10 | 11 | 12 | has_private_storage () { 13 | expect_vars HALCYON_NO_PRIVATE_STORAGE 14 | 15 | ! (( HALCYON_NO_PRIVATE_STORAGE )) || return 1 16 | [[ -n "${HALCYON_AWS_ACCESS_KEY_ID:+_}" ]] || return 1 17 | [[ -n "${HALCYON_AWS_SECRET_ACCESS_KEY:+_}" ]] || return 1 18 | [[ -n "${HALCYON_S3_BUCKET:+_}" ]] || return 1 19 | [[ -n "${HALCYON_S3_ENDPOINT:+_}" ]] || return 1 20 | [[ -n "${HALCYON_S3_ACL:+_}" ]] || return 1 21 | } 22 | 23 | 24 | has_not_public_private_storage () { 25 | has_private_storage || return 1 26 | 27 | if has_public_storage && 28 | [[ "${HALCYON_S3_BUCKET}" == "${HALCYON_DEFAULT_PUBLIC_STORAGE_S3_BUCKET}" ]] 29 | then 30 | return 1 31 | fi 32 | } 33 | 34 | 35 | format_public_storage_url () { 36 | expect_vars HALCYON_PUBLIC_STORAGE_URL 37 | 38 | local object 39 | expect_args object -- "$@" 40 | 41 | echo "${HALCYON_PUBLIC_STORAGE_URL}/${object}" 42 | } 43 | 44 | 45 | describe_storage () { 46 | if has_private_storage && has_public_storage; then 47 | log_indent_label 'External storage:' 'private and public' 48 | elif has_private_storage; then 49 | log_indent_label 'External storage:' 'private' 50 | elif has_public_storage; then 51 | log_indent_label 'External storage:' 'public' 52 | else 53 | log_indent_label 'External storage:' 'none' 54 | fi 55 | } 56 | 57 | 58 | create_cached_archive () { 59 | expect_vars HALCYON_CACHE 60 | 61 | local src_dir dst_file_name 62 | expect_args src_dir dst_file_name -- "$@" 63 | 64 | expect_existing "${src_dir}" || return 1 65 | 66 | if ! create_archive "${src_dir}" "${HALCYON_CACHE}/${dst_file_name}"; then 67 | log_error 'Failed to create cached archive' 68 | return 1 69 | fi 70 | } 71 | 72 | 73 | extract_cached_archive_over () { 74 | expect_vars HALCYON_CACHE 75 | 76 | local src_file_name dst_dir 77 | expect_args src_file_name dst_dir -- "$@" 78 | 79 | if [[ ! -f "${HALCYON_CACHE}/${src_file_name}" ]]; then 80 | return 1 81 | fi 82 | 83 | if ! extract_archive_over "${HALCYON_CACHE}/${src_file_name}" "${dst_dir}"; then 84 | log_error 'Failed to extract cached archive' 85 | return 1 86 | fi 87 | } 88 | 89 | 90 | touch_cached_file () { 91 | expect_vars HALCYON_CACHE 92 | 93 | local file_name 94 | expect_args file_name -- "$@" 95 | 96 | if [[ ! -f "${HALCYON_CACHE}/${file_name}" ]]; then 97 | return 0 98 | fi 99 | 100 | touch "${HALCYON_CACHE}/${file_name}" || return 0 101 | } 102 | 103 | 104 | touch_cached_ghc_and_cabal_files () { 105 | expect_vars HALCYON_CACHE 106 | 107 | local name 108 | find_tree "${HALCYON_CACHE}" -maxdepth 1 -type f | 109 | filter_matching "^(halcyon-ghc-.*|halcyon-cabal-.*)$" | 110 | while read -r name; do 111 | touch "${HALCYON_CACHE}/${name}" || true 112 | done || return 0 113 | } 114 | 115 | 116 | check_s3_status () { 117 | local s3_status 118 | expect_args s3_status -- "$@" 119 | 120 | # NOTE: Requests using the wrong S3 endpoint fail with 301. 121 | # https://github.com/mietek/haskell-on-heroku/issues/37 122 | if (( s3_status == 3 )); then 123 | log_error "Incorrect HALCYON_S3_ENDPOINT for ${HALCYON_S3_BUCKET}: ${HALCYON_S3_ENDPOINT}" 124 | log_error "To use ${HALCYON_S3_BUCKET}, use the correct HALCYON_S3_ENDPOINT" 125 | return 1 126 | fi 127 | } 128 | 129 | 130 | upload_cached_file () { 131 | expect_vars HALCYON_CACHE HALCYON_NO_UPLOAD 132 | 133 | local prefix file_name 134 | expect_args prefix file_name -- "$@" 135 | 136 | if (( HALCYON_NO_UPLOAD )) || ! has_private_storage; then 137 | return 0 138 | fi 139 | 140 | local object file 141 | object="${prefix:+${prefix}/}${file_name}" 142 | file="${HALCYON_CACHE}/${file_name}" 143 | 144 | local status 145 | status=0 146 | s3_upload "${file}" "${HALCYON_S3_BUCKET}" "${object}" "${HALCYON_S3_ACL}" || status="$?" 147 | if (( status )); then 148 | log_error 'Failed to upload cached file' 149 | check_s3_status "${status}" || return 1 150 | return 1 151 | fi 152 | } 153 | 154 | 155 | cache_stored_file () { 156 | expect_vars HALCYON_CACHE 157 | 158 | local prefix file_name 159 | expect_args prefix file_name -- "$@" 160 | 161 | local object file 162 | object="${prefix:+${prefix}/}${file_name}" 163 | file="${HALCYON_CACHE}/${file_name}" 164 | 165 | if has_not_public_private_storage; then 166 | local status 167 | status=0 168 | s3_download "${HALCYON_S3_BUCKET}" "${object}" "${file}" || status="$?" 169 | if ! (( status )); then 170 | return 0 171 | fi 172 | check_s3_status "${status}" || return 1 173 | fi 174 | 175 | if ! has_public_storage; then 176 | return 1 177 | fi 178 | 179 | local public_url 180 | public_url=$( format_public_storage_url "${object}" ) 181 | 182 | curl_download "${public_url}" "${file}" || return 1 183 | 184 | if has_not_public_private_storage; then 185 | upload_cached_file "${prefix}" "${file_name}" || return 1 186 | fi 187 | } 188 | 189 | 190 | cache_original_stored_file () { 191 | expect_vars HALCYON_CACHE 192 | 193 | local original_url 194 | expect_args original_url -- "$@" 195 | 196 | local file_name file 197 | file_name=$( basename "${original_url}" ) || return 1 198 | file="${HALCYON_CACHE}/${file_name}" 199 | 200 | if cache_stored_file 'original' "${file_name}"; then 201 | return 0 202 | fi 203 | 204 | curl_download "${original_url}" "${file}" || return 1 205 | upload_cached_file 'original' "${file_name}" || return 1 206 | } 207 | 208 | 209 | acquire_original_source () { 210 | local original_url dst_dir 211 | expect_args original_url dst_dir -- "$@" 212 | 213 | local original_name 214 | original_name=$( basename "${original_url}" ) || return 1 215 | 216 | if ! extract_cached_archive_over "${original_name}" "${dst_dir}"; then 217 | rm -rf "${dst_dir}" || true 218 | cache_original_stored_file "${original_url}" || return 1 219 | 220 | if ! extract_cached_archive_over "${original_name}" "${dst_dir}"; then 221 | rm -rf "${dst_dir}" || true 222 | 223 | log_error 'Failed to acquire original source' 224 | return 1 225 | fi 226 | else 227 | touch_cached_file "${original_name}" 228 | fi 229 | } 230 | 231 | 232 | delete_private_stored_file () { 233 | expect_vars HALCYON_NO_UPLOAD HALCYON_NO_CLEAN_PRIVATE_STORAGE 234 | 235 | local prefix file_name 236 | expect_args prefix file_name -- "$@" 237 | 238 | if (( HALCYON_NO_UPLOAD )) || 239 | (( HALCYON_NO_CLEAN_PRIVATE_STORAGE )) || 240 | ! has_private_storage 241 | then 242 | return 0 243 | fi 244 | 245 | local object status 246 | object="${prefix:+${prefix}/}${file_name}" 247 | status=0 248 | s3_delete "${HALCYON_S3_BUCKET}" "${object}" || status="$?" 249 | if (( status )); then 250 | log_error 'Failed to delete private stored file' 251 | check_s3_status "${status}" || return 1 252 | return 1 253 | fi 254 | } 255 | 256 | 257 | list_private_stored_files () { 258 | local prefix 259 | expect_args prefix -- "$@" 260 | 261 | if ! has_private_storage; then 262 | return 0 263 | fi 264 | 265 | local status 266 | status=0 267 | s3_list "${HALCYON_S3_BUCKET}" "${prefix}" || status="$?" 268 | if (( status )); then 269 | log_error 'Failed to list private stored files' 270 | check_s3_status "${status}" || return 1 271 | return 1 272 | fi 273 | } 274 | 275 | 276 | list_public_stored_files () { 277 | local prefix 278 | expect_args prefix -- "$@" 279 | 280 | if ! has_public_storage; then 281 | return 0 282 | fi 283 | 284 | local public_url 285 | public_url=$( format_public_storage_url "${prefix:+?prefix=${prefix}}" ) 286 | 287 | if ! curl_list_s3 "${public_url}"; then 288 | log_error 'Failed to list public stored files' 289 | return 1 290 | fi 291 | } 292 | 293 | 294 | list_stored_files () { 295 | local prefix 296 | expect_args prefix -- "$@" 297 | 298 | local private_files 299 | if ! private_files=$( list_private_stored_files "${prefix}" ); then 300 | return 1 301 | fi 302 | 303 | echo "${private_files}" 304 | 305 | local public_files 306 | if ! public_files=$( list_public_stored_files "${prefix}" ); then 307 | return 1 308 | fi 309 | 310 | echo "${public_files}" 311 | } 312 | 313 | 314 | delete_matching_private_stored_files () { 315 | expect_vars HALCYON_NO_UPLOAD HALCYON_NO_CLEAN_PRIVATE_STORAGE 316 | 317 | local prefix match_prefix match_pattern save_name 318 | expect_args prefix match_prefix match_pattern save_name -- "$@" 319 | 320 | if (( HALCYON_NO_UPLOAD )) || 321 | (( HALCYON_NO_CLEAN_PRIVATE_STORAGE )) || 322 | ! has_private_storage 323 | then 324 | return 0 325 | fi 326 | 327 | local old_name 328 | list_private_stored_files "${prefix}/${match_prefix}" | 329 | sed "s:^${prefix}/::" | 330 | filter_matching "^${match_pattern}$" | 331 | filter_not_matching "^${save_name//./\.}$" | 332 | while read -r old_name; do 333 | delete_private_stored_file "${prefix}" "${old_name}" || return 1 334 | done || return 0 335 | } 336 | 337 | 338 | prepare_cache () { 339 | expect_vars HALCYON_CACHE HALCYON_PURGE_CACHE HALCYON_NO_CLEAN_CACHE \ 340 | HALCYON_INTERNAL_RECURSIVE 341 | 342 | local cache_dir 343 | expect_args cache_dir -- "$@" 344 | 345 | if (( HALCYON_NO_CLEAN_CACHE )) || (( HALCYON_INTERNAL_RECURSIVE )); then 346 | return 0 347 | fi 348 | 349 | if (( HALCYON_PURGE_CACHE )); then 350 | log 'Purging cache' 351 | log 352 | 353 | rm -rf "${HALCYON_CACHE}" || return 1 354 | fi 355 | 356 | mkdir -p "${HALCYON_CACHE}" "${cache_dir}" || return 1 357 | 358 | if ! (( HALCYON_PURGE_CACHE )); then 359 | local files 360 | if files=$( 361 | find_tree "${HALCYON_CACHE}" -maxdepth 1 -type f | 362 | sort_natural | 363 | match_at_least_one 364 | ); then 365 | log 'Examining cache contents' 366 | 367 | copy_dir_over "${HALCYON_CACHE}" "${cache_dir}" || return 1 368 | 369 | quote <<<"${files}" 370 | log 371 | fi 372 | fi 373 | 374 | touch "${cache_dir}" || return 1 375 | } 376 | 377 | 378 | clean_cache () { 379 | expect_vars HALCYON_CACHE HALCYON_NO_CLEAN_CACHE \ 380 | HALCYON_INTERNAL_RECURSIVE 381 | 382 | local cache_dir 383 | expect_args cache_dir -- "$@" 384 | 385 | if (( HALCYON_NO_CLEAN_CACHE )) || (( HALCYON_INTERNAL_RECURSIVE )); then 386 | return 0 387 | fi 388 | 389 | local mark_time 390 | mark_time=$( get_modification_time "${cache_dir}" ) || return 1 391 | 392 | rm -f "${HALCYON_CACHE}/"*'.constraints' || true 393 | 394 | local file 395 | find "${HALCYON_CACHE}" -maxdepth 1 -type f 2>'/dev/null' | 396 | while read -r file; do 397 | local file_time 398 | if file_time=$( get_modification_time "${file}" ) && 399 | (( file_time < mark_time )) 400 | then 401 | rm -f "${file}" || true 402 | fi 403 | done 404 | 405 | local -a opts_a 406 | opts_a=() 407 | opts_a+=( \( -name 'apt' ) 408 | opts_a+=( \) -prune -o ) 409 | 410 | local changed_files 411 | if changed_files=$( 412 | compare_tree "${cache_dir}" "${HALCYON_CACHE}" "${opts_a[@]}" | 413 | filter_not_matching '^= ' | 414 | match_at_least_one 415 | ); then 416 | log 417 | log 'Examining cache changes' 418 | 419 | quote <<<"${changed_files}" 420 | fi 421 | } 422 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | export HALCYON_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd -P ) 6 | 7 | source "${HALCYON_DIR}/src.sh" 8 | 9 | 10 | test_cabal () { 11 | local -a cabal_versions_a 12 | cabal_versions_a=() 13 | cabal_versions_a+=( '1.20.0.0' ) 14 | cabal_versions_a+=( '1.20.0.1' ) 15 | cabal_versions_a+=( '1.20.0.3' ) 16 | cabal_versions_a+=( '1.20.0.5' ) 17 | cabal_versions_a+=( '1.20.0.6' ) 18 | cabal_versions_a+=( '1.20.1.0' ) 19 | cabal_versions_a+=( '1.22.0.0' ) 20 | cabal_versions_a+=( '1.22.0.1' ) 21 | cabal_versions_a+=( '1.22.2.0' ) 22 | cabal_versions_a+=( '1.22.3.0' ) 23 | cabal_versions_a+=( '1.22.4.0' ) 24 | cabal_versions_a+=( '1.22.5.0' ) 25 | cabal_versions_a+=( '1.22.6.0' ) 26 | 27 | log 'Testing Cabal' 28 | 29 | for cabal_version in "${cabal_versions_a[@]}"; do 30 | local -a opts_a 31 | opts_a=() 32 | opts_a+=( --cabal-version="${cabal_version}" ) 33 | case "${cabal_version}" in 34 | '1.20.0.3'|'1.22.6.0') 35 | true 36 | ;; 37 | *) 38 | opts_a+=( --cabal-no-update ) 39 | esac 40 | 41 | log 42 | halcyon install "${opts_a[@]}" "$@" || return 1 43 | done 44 | } 45 | 46 | 47 | test_ghc () { 48 | expect_vars HALCYON_INTERNAL_PLATFORM 49 | local -a ghc_versions_a 50 | ghc_versions_a=() 51 | ghc_versions_a+=( '7.0.4' ) 52 | ghc_versions_a+=( '7.2.2' ) 53 | ghc_versions_a+=( '7.4.2' ) 54 | ghc_versions_a+=( '7.6.1' ) 55 | ghc_versions_a+=( '7.6.3' ) 56 | ghc_versions_a+=( '7.8.2' ) 57 | ghc_versions_a+=( '7.8.3' ) 58 | ghc_versions_a+=( '7.8.4' ) 59 | case "${HALCYON_INTERNAL_PLATFORM}" in 60 | 'linux-centos-6'*'-i386'|'linux-debian-6'*|'linux-ubuntu-10'*) 61 | true 62 | ;; 63 | *) 64 | ghc_versions_a+=( '7.10.1' ) 65 | ghc_versions_a+=( '7.10.2-rc1' ) 66 | esac 67 | 68 | log 'Testing GHC' 69 | 70 | for ghc_version in "${ghc_versions_a[@]}"; do 71 | local ghc_major ghc_minor 72 | ghc_major="${ghc_version%%.*}" 73 | ghc_minor="${ghc_version#*.}" 74 | ghc_minor="${ghc_minor%%.*}" 75 | 76 | local -a opts_a 77 | opts_a=() 78 | opts_a+=( --ghc-version="${ghc_version}" ) 79 | if (( ghc_major == 7 && ghc_minor >= 10 )); then 80 | opts_a+=( --cabal-version='1.22.6.0' ) 81 | fi 82 | 83 | log 84 | halcyon install "${opts_a[@]}" "$@" || return 1 85 | done 86 | } 87 | 88 | 89 | test_tutorial () { 90 | local -a urls_a 91 | urls_a=() 92 | urls_a+=( "https://github.com/mietek/halcyon-tutorial" ) 93 | urls_a+=( "https://github.com/mietek/haskell-on-heroku-tutorial" ) 94 | 95 | log 'Testing tutorial' 96 | 97 | for url in "${urls_a[@]}"; do 98 | log 99 | halcyon install "${url}" "$@" || return 1 100 | done 101 | } 102 | 103 | 104 | test_sandbox_apps () { 105 | expect_vars HALCYON_BASE 106 | 107 | local -a names_a 108 | names_a=() 109 | names_a+=( 'alex' ) 110 | names_a+=( 'happy' ) 111 | names_a+=( 'c2hs' ) 112 | names_a+=( 'cpphs' ) 113 | names_a+=( 'hscolour' ) 114 | names_a+=( 'haddock' ) 115 | names_a+=( 'greencard' ) 116 | 117 | local constraints_dir 118 | constraints_dir=$( get_tmp_dir 'halcyon-constraints' ) || return 1 119 | 120 | log 'Testing sandbox apps' 121 | 122 | mkdir -p "${constraints_dir}" || return 1 123 | 124 | local -A labels_A 125 | labels_A=() 126 | for name in "${names_a[@]}"; do 127 | local label 128 | label=$( halcyon label "${name}" 2>'/dev/null' ) || return 1 129 | labels_A[${name}]="${label}" 130 | halcyon constraints "${label}" >"${constraints_dir}/${label}.constraints" 2>'/dev/null' || return 1 131 | 132 | local -a opts_a 133 | opts_a=() 134 | opts_a+=( --constraints="${constraints_dir}" ) 135 | opts_a+=( --prefix="${HALCYON_BASE}/sandbox" ) 136 | if [[ "${name}" == 'c2hs' ]]; then 137 | opts_a+=( --sandbox-extra-apps="${labels_A[alex]} ${labels_A[happy]}" ) 138 | opts_a+=( --sandbox-extra-apps-constraints="${constraints_dir}" ) 139 | fi 140 | 141 | log 142 | halcyon install "${opts_a[@]}" "${label}" "$@" || return 1 143 | done 144 | } 145 | 146 | 147 | test_apps () { 148 | local -a names_a 149 | names_a=() 150 | names_a+=( 'keter' ) 151 | names_a+=( 'pandoc' ) 152 | 153 | local constraints_dir 154 | constraints_dir=$( get_tmp_dir 'halcyon-constraints' ) || return 1 155 | 156 | log 'Testing apps' 157 | 158 | mkdir -p "${constraints_dir}" || return 1 159 | 160 | local -A labels_A 161 | labels_A=() 162 | for name in "${names_a[@]}"; do 163 | local label 164 | label=$( halcyon label "${name}" 2>'/dev/null' ) || return 1 165 | labels_A[${name}]="${label}" 166 | halcyon constraints "${label}" >"${constraints_dir}/${label}.constraints" 2>'/dev/null' || return 1 167 | 168 | local -a opts_a 169 | opts_a=() 170 | opts_a+=( --constraints="${constraints_dir}" ) 171 | 172 | log 173 | halcyon install "${opts_a[@]}" "${label}" "$@" || return 1 174 | done 175 | } 176 | 177 | 178 | bench_first_build () { 179 | expect_vars HALCYON_BASE 180 | 181 | local url csv_file 182 | expect_args url csv_file -- "$@" 183 | shift 2 184 | 185 | local name log_file 186 | name=$( basename "${url}" ) || return 1 187 | log_file=$( get_tmp_file 'halcyon-shootout' )'.log' || return 1 188 | 189 | rm -rf "${HALCYON_BASE}/ghc" "${HALCYON_BASE}/cabal" || return 1 190 | 191 | HALCYON_SANDBOX_REBUILD=1 \ 192 | HALCYON_NO_UPLOAD=1 \ 193 | halcyon install "${url}" "$@" 2>&1 | tee "${log_file}" || return 1 194 | 195 | local restore_time sandbox_time build_time install_time 196 | restore_time=$( filter_matching 'Building sandbox directory$' <"${log_file}" | awk '{ print $2 }' ) 197 | sandbox_time=$( filter_matching 'Configuring app$' <"${log_file}" | awk '{ print $2 }' ) 198 | build_time=$( filter_matching 'Preparing install directory$' <"${log_file}" | awk '{ print $2 }' ) 199 | install_time=$( filter_matching 'App installed:' <"${log_file}" | awk '{ print $2 }' ) 200 | 201 | local sandbox_size app_size 202 | sandbox_size=$( filter_matching 'Sandbox built,' <"${log_file}" | awk '{ print $6 }' ) 203 | app_size=$( filter_matching 'App built,' <"${log_file}" | awk '{ print $6 }' ) 204 | 205 | echo "${name},${restore_time},${sandbox_time},${build_time},${install_time},${sandbox_size},${app_size}" >>"${csv_file}" 206 | } 207 | 208 | 209 | test_shootout () { 210 | local bench branch 211 | expect_args bench branch -- "$@" 212 | shift 2 213 | 214 | local -a urls_a 215 | urls_a=() 216 | if ! (( bench )); then 217 | # NOTE: When not benchmarking, hello-wai goes first, so that 218 | # the sandbox can be reused to speed up the other builds. 219 | urls_a+=( "https://github.com/mietek/hello-wai${branch}" ) 220 | fi 221 | urls_a+=( "https://github.com/mietek/hello-apiary${branch}" ) 222 | urls_a+=( "https://github.com/mietek/hello-happstack${branch}" ) 223 | urls_a+=( "https://github.com/mietek/hello-mflow${branch}" ) 224 | urls_a+=( "https://github.com/mietek/hello-miku${branch}" ) 225 | urls_a+=( "https://github.com/mietek/hello-scotty${branch}" ) 226 | urls_a+=( "https://github.com/mietek/hello-servant${branch}" ) 227 | urls_a+=( "https://github.com/mietek/hello-simple${branch}" ) 228 | urls_a+=( "https://github.com/mietek/hello-snap${branch}" ) 229 | urls_a+=( "https://github.com/mietek/hello-spock${branch}" ) 230 | if (( bench )); then 231 | urls_a+=( "https://github.com/mietek/hello-wai${branch}" ) 232 | fi 233 | urls_a+=( "https://github.com/mietek/hello-wheb${branch}" ) 234 | urls_a+=( "https://github.com/mietek/hello-yesod${branch}" ) 235 | 236 | local csv_file 237 | csv_file='' 238 | if (( bench )); then 239 | csv_file=$( get_tmp_file 'halcyon-shootout' )'.csv' || return 1 240 | fi 241 | 242 | log 'Testing shootout' 243 | 244 | for url in "${urls_a[@]}"; do 245 | log 246 | if (( bench )); then 247 | bench_first_build "${url}" "${csv_file}" "$@" || return 1 248 | else 249 | halcyon install "${url}" "$@" || return 1 250 | fi 251 | done 252 | 253 | if (( bench )); then 254 | cat "${csv_file}" 255 | fi 256 | } 257 | 258 | 259 | test_examples () { 260 | local branch 261 | expect_args branch -- "$@" 262 | shift 263 | 264 | local -a urls_a 265 | urls_a=() 266 | urls_a+=( "https://github.com/mietek/howistart${branch}" ) 267 | urls_a+=( "https://github.com/mietek/hl${branch}" ) 268 | urls_a+=( "https://github.com/mietek/tryhaskell${branch}" ) 269 | urls_a+=( "https://github.com/mietek/tryidris${branch}" ) 270 | urls_a+=( "https://github.com/mietek/trypurescript${branch}" ) 271 | urls_a+=( "https://github.com/mietek/tryhaste${branch}" ) 272 | urls_a+=( "https://github.com/mietek/gitit${branch}" ) 273 | 274 | log 'Testing examples' 275 | 276 | for url in "${urls_a[@]}"; do 277 | log 278 | halcyon install "${url}" "$@" || return 1 279 | done 280 | } 281 | 282 | 283 | help_test_usage () { 284 | cat >&2 <<-EOF 285 | Usage: 286 | test COMMAND 287 | 288 | Commands: 289 | cabal 290 | ghc 291 | tutorial 292 | sandbox-apps 293 | apps 294 | shootout --bench? --branch? 295 | examples --branch? 296 | all 297 | EOF 298 | } 299 | 300 | 301 | test_main () { 302 | local cmd bench branch 303 | local -a args_a 304 | cmd='' 305 | bench=0 306 | branch='' 307 | args_a=() 308 | 309 | while (( $# )); do 310 | case "$1" in 311 | 312 | '--bench') 313 | bench=1;; 314 | '--branch') 315 | shift 316 | expect_args branch -- "$@";; 317 | '--branch='*) 318 | branch="${1#*=}";; 319 | 320 | '-h'|'--help') 321 | help_usage 322 | return 0 323 | ;; 324 | '--') 325 | shift 326 | while (( $# )); do 327 | if [[ -z "${cmd}" ]]; then 328 | cmd="$1" 329 | else 330 | args_a+=( "$1" ) 331 | fi 332 | shift 333 | done 334 | break 335 | ;; 336 | '-'*) 337 | log_error "Unexpected option: $1" 338 | log 339 | help_test_usage 340 | return 1 341 | ;; 342 | *) 343 | if [[ -z "${cmd}" ]]; then 344 | cmd="$1" 345 | else 346 | args_a+=( "$1" ) 347 | fi 348 | esac 349 | shift 350 | done 351 | 352 | if [[ -n "${branch}" && ! "${branch}" =~ ^# ]]; then 353 | branch="#${branch}" 354 | fi 355 | 356 | case "${cmd}" in 357 | 'cabal') 358 | test_cabal ${args_a[@]:+"${args_a[@]}"} || return 1 359 | ;; 360 | 'ghc') 361 | test_ghc ${args_a[@]:+"${args_a[@]}"} || return 1 362 | ;; 363 | 'tutorial') 364 | test_tutorial ${args_a[@]:+"${args_a[@]}"} || return 1 365 | ;; 366 | 'sandbox-apps') 367 | test_sandbox_apps ${args_a[@]:+"${args_a[@]}"} || return 1 368 | ;; 369 | 'apps') 370 | test_apps ${args_a[@]:+"${args_a[@]}"} || return 1 371 | ;; 372 | 'shootout') 373 | test_shootout "${bench}" "${branch}" ${args_a[@]:+"${args_a[@]}"} || return 1 374 | ;; 375 | 'examples') 376 | test_examples "${branch}" ${args_a[@]:+"${args_a[@]}"} || return 1 377 | ;; 378 | 'all') 379 | test_cabal ${args_a[@]:+"${args_a[@]}"} || return 1 380 | test_ghc ${args_a[@]:+"${args_a[@]}"} || return 1 381 | test_tutorial ${args_a[@]:+"${args_a[@]}"} || return 1 382 | test_sandbox_apps ${args_a[@]:+"${args_a[@]}"} || return 1 383 | test_apps ${args_a[@]:+"${args_a[@]}"} || return 1 384 | test_shootout "${bench}" "${branch}" ${args_a[@]:+"${args_a[@]}"} || return 1 385 | test_examples "${branch}" ${args_a[@]:+"${args_a[@]}"} || return 1 386 | ;; 387 | 'help') 388 | help_test_usage 389 | return 0 390 | ;; 391 | '') 392 | log_error 'Expected command' 393 | log 394 | help_test_usage 395 | return 1 396 | ;; 397 | *) 398 | log_error "Unexpected command: ${cmd} ${args_a[*]:-}" 399 | log 400 | help_test_usage 401 | return 1 402 | esac 403 | } 404 | 405 | 406 | HALCYON_LOG_TIMESTAMP=1 \ 407 | HALCYON_NO_SELF_UPDATE=1 \ 408 | HALCYON_NO_CLEAN_CACHE=1 \ 409 | test_main "$@" 410 | -------------------------------------------------------------------------------- /src/constraints.sh: -------------------------------------------------------------------------------- 1 | read_constraints () { 2 | sed 's/^\(.*\)-\(.*\)$/\1 \2/' || return 0 3 | } 4 | 5 | 6 | read_constraints_from_cabal_freeze () { 7 | awk '/^ *[Cc]onstraints:/, !/[:,]/ { print }' | 8 | tr -d '\r' | 9 | sed 's/[Cc]onstraints://;s/,//g;s/==/ /;s/ */ /g;s/^ //;/^ ?$/d' || return 0 10 | } 11 | 12 | 13 | read_constraints_from_cabal_dry_freeze () { 14 | awk '/The following packages would be frozen:/ { i = 1 } i' | 15 | filter_not_first | 16 | sed 's/ == / /' || return 0 17 | } 18 | 19 | 20 | match_package_version () { 21 | local package_name 22 | expect_args package_name -- "$@" 23 | 24 | filter_matching "^${package_name} " | 25 | match_exactly_one | 26 | sed 's/^.* //' || return 1 27 | } 28 | 29 | 30 | filter_correct_constraints () { 31 | local label 32 | expect_args label -- "$@" 33 | 34 | # NOTE: Cabal includes the package itself in the list of frozen 35 | # constraints. 36 | # https://github.com/haskell/cabal/issues/1908 37 | local name version 38 | name="${label%-*}" 39 | version="${label##*-}" 40 | 41 | filter_not_matching "^${name} ${version}$" 42 | } 43 | 44 | 45 | format_constraints () { 46 | sed 's/ /-/' || return 0 47 | } 48 | 49 | 50 | format_constraints_to_cabal_freeze () { 51 | awk 'BEGIN { printf "constraints:"; separator = " " } 52 | !/^$/ { printf "%s%s ==%s", separator, $1, $2; separator = ",\n " } 53 | END { printf "\n" }' || return 0 54 | } 55 | 56 | 57 | hash_constraints () { 58 | local constraints 59 | expect_args constraints -- "$@" 60 | 61 | local constraints_hash 62 | if ! constraints_hash=$( get_hash <<<"${constraints}" ); then 63 | log_error 'Failed to hash constraints' 64 | return 1 65 | fi 66 | 67 | echo "${constraints_hash}" 68 | } 69 | 70 | 71 | prepare_constraints () { 72 | expect_vars HALCYON_IGNORE_ALL_CONSTRAINTS 73 | 74 | local label source_dir 75 | expect_args label source_dir -- "$@" 76 | 77 | local magic_dir 78 | magic_dir="${source_dir}/.halcyon" 79 | 80 | if [[ -n "${HALCYON_CONSTRAINTS:+_}" ]]; then 81 | if [[ -d "${HALCYON_CONSTRAINTS}" ]]; then 82 | copy_file "${HALCYON_CONSTRAINTS}/${label}.constraints" "${magic_dir}/constraints" || return 1 83 | elif [[ -f "${HALCYON_CONSTRAINTS}" ]]; then 84 | copy_file "${HALCYON_CONSTRAINTS}" "${magic_dir}/constraints" || return 1 85 | else 86 | copy_file <( echo "${HALCYON_CONSTRAINTS}" ) "${magic_dir}/constraints" || return 1 87 | fi 88 | fi 89 | 90 | if (( HALCYON_IGNORE_ALL_CONSTRAINTS )); then 91 | rm -f "${source_dir}/cabal.config" || return 1 92 | return 0 93 | fi 94 | 95 | if [[ -f "${magic_dir}/constraints" ]]; then 96 | read_constraints <"${magic_dir}/constraints" | 97 | sort_natural | 98 | format_constraints_to_cabal_freeze >"${source_dir}/cabal.config" || return 1 99 | fi 100 | } 101 | 102 | 103 | detect_constraints () { 104 | local label source_dir 105 | expect_args label source_dir -- "$@" 106 | 107 | expect_existing "${source_dir}/cabal.config" || return 1 108 | 109 | local candidate_constraints 110 | candidate_constraints=$( 111 | read_constraints_from_cabal_freeze <"${source_dir}/cabal.config" | 112 | filter_correct_constraints "${label}" | 113 | sort_natural 114 | ) || return 1 115 | if [[ -z "${candidate_constraints}" ]]; then 116 | return 0 117 | fi 118 | 119 | local -a constraints_a 120 | local -A packages_A 121 | local candidate_package candidate_version 122 | constraints_a=() 123 | packages_A=() 124 | while read -r candidate_package candidate_version; do 125 | if [[ "${candidate_version}" == 'installed' ]]; then 126 | log_warning "Ignoring installed constraint: ${candidate_package}" 127 | elif [[ ! "${candidate_version}" =~ [0-9]+(\.[0-9]+)* ]]; then 128 | log_warning "Ignoring unexpected constraint: ${candidate_package}-${candidate_version}" 129 | elif [[ -n "${packages_A[${candidate_package}]:+_}" ]]; then 130 | log_warning "Ignoring duplicate constraint: ${candidate_package}-${candidate_version} (${packages_A[${candidate_package}]})" 131 | else 132 | constraints_a+=( "${candidate_package} ${candidate_version}" ) 133 | packages_A["${candidate_package}"]="${candidate_version}" 134 | fi 135 | done <<<"${candidate_constraints}" 136 | if [[ -z "${constraints_a[@]:+_}" ]]; then 137 | return 0 138 | fi 139 | 140 | IFS=$'\n' && echo "${constraints_a[*]}" 141 | } 142 | 143 | 144 | validate_actual_constraints () { 145 | local tag source_dir constraints 146 | expect_args tag source_dir constraints -- "$@" 147 | 148 | local label actual_constraints constraints_hash actual_hash 149 | label=$( get_tag_label "${tag}" ) 150 | constraints_hash=$( get_tag_constraints_hash "${tag}" ) 151 | if ! actual_constraints=$( sandboxed_cabal_dry_freeze_constraints "${label}" "${source_dir}" ) || 152 | ! actual_hash=$( hash_constraints "${actual_constraints}" ) 153 | then 154 | log_warning 'Failed to determine actual constraints' 155 | return 0 156 | fi 157 | 158 | # NOTE: Cabal sometimes gives different results when freezing 159 | # constraints before and after installation. 160 | # https://github.com/haskell/cabal/issues/1896 161 | # https://github.com/mietek/halcyon/issues/1 162 | if [[ "${actual_hash}" != "${constraints_hash}" ]]; then 163 | log_warning 'Unexpected constraints difference' 164 | diff --unified \ 165 | <( format_constraints <<<"${constraints}" ) \ 166 | <( format_constraints <<<"${actual_constraints}" ) | 167 | filter_not_first | 168 | filter_not_first | 169 | quote || true 170 | fi 171 | } 172 | 173 | 174 | validate_full_constraints_file () { 175 | local tag candidate_file 176 | expect_args tag candidate_file -- "$@" 177 | 178 | if [[ ! -f "${candidate_file}" ]]; then 179 | return 1 180 | fi 181 | 182 | local candidate_constraints 183 | candidate_constraints=$( read_constraints <"${candidate_file}" ) || return 1 184 | 185 | local constraints_hash candidate_hash 186 | constraints_hash=$( get_tag_constraints_hash "${tag}" ) 187 | candidate_hash=$( hash_constraints "${candidate_constraints}" ) || return 1 188 | 189 | if [[ "${candidate_hash}" != "${constraints_hash}" ]]; then 190 | return 1 191 | fi 192 | 193 | echo "${candidate_hash}" 194 | } 195 | 196 | 197 | validate_partial_constraints_file () { 198 | local candidate_file 199 | expect_args candidate_file -- "$@" 200 | 201 | if [[ ! -f "${candidate_file}" ]]; then 202 | return 1 203 | fi 204 | 205 | local candidate_constraints 206 | candidate_constraints=$( read_constraints <"${candidate_file}" ) || return 1 207 | 208 | local constraints_name short_hash_etc short_hash candidate_hash 209 | constraints_name=$( basename "${candidate_file}" ) || return 1 210 | short_hash_etc="${constraints_name#halcyon-sandbox-}" 211 | short_hash="${short_hash_etc%%[-.]*}" 212 | candidate_hash=$( hash_constraints "${candidate_constraints}" ) || return 1 213 | 214 | if [[ "${candidate_hash:0:7}" != "${short_hash}" ]]; then 215 | return 1 216 | fi 217 | 218 | echo "${candidate_hash}" 219 | } 220 | 221 | 222 | match_full_sandbox_dir () { 223 | expect_vars HALCYON_CACHE \ 224 | HALCYON_INTERNAL_PLATFORM 225 | 226 | local tag all_names 227 | expect_args tag all_names -- "$@" 228 | 229 | local ghc_id full_pattern full_names 230 | ghc_id=$( format_ghc_id "${tag}" ) 231 | full_pattern=$( format_full_sandbox_constraints_file_name_pattern "${tag}" ) 232 | full_names=$( 233 | filter_matching "^${full_pattern}$" <<<"${all_names}" | 234 | match_at_least_one 235 | ) || return 1 236 | 237 | log 'Examining fully-matching sandbox directories' 238 | 239 | local full_name 240 | while read -r full_name; do 241 | local full_file full_hash 242 | full_file="${HALCYON_CACHE}/${full_name}" 243 | if ! full_hash=$( validate_full_constraints_file "${tag}" "${full_file}" ); then 244 | rm -f "${full_file}" || true 245 | 246 | if ! HALCYON_NO_UPLOAD=1 \ 247 | cache_stored_file "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${full_name}" || 248 | ! full_hash=$( validate_full_constraints_file "${tag}" "${full_file}" ) 249 | then 250 | rm -f "${full_file}" || true 251 | continue 252 | fi 253 | else 254 | touch_cached_file "${full_name}" 255 | fi 256 | 257 | local full_label full_tag 258 | full_label=$( format_sandbox_constraints_file_name_label "${full_name}" ) 259 | full_tag=$( derive_matching_sandbox_tag "${tag}" "${full_label}" "${full_hash}" ) 260 | 261 | echo "${full_tag}" 262 | return 0 263 | done <<<"${full_names}" 264 | 265 | return 1 266 | } 267 | 268 | 269 | list_partial_sandbox_dirs () { 270 | expect_vars HALCYON_CACHE \ 271 | HALCYON_INTERNAL_PLATFORM 272 | 273 | local tag constraints all_names 274 | expect_args tag constraints all_names -- "$@" 275 | 276 | local ghc_id full_pattern partial_names 277 | ghc_id=$( format_ghc_id "${tag}" ) 278 | full_pattern=$( format_full_sandbox_constraints_file_name_pattern "${tag}" ) 279 | partial_names=$( 280 | filter_not_matching "^${full_pattern}$" <<<"${all_names}" | 281 | match_at_least_one 282 | ) || return 0 283 | 284 | log 'Examining partially-matching sandbox directories' 285 | 286 | local partial_name 287 | while read -r partial_name; do 288 | local partial_file partial_hash 289 | partial_file="${HALCYON_CACHE}/${partial_name}" 290 | if ! partial_hash=$( validate_partial_constraints_file "${partial_file}" ); then 291 | rm -f "${partial_file}" || true 292 | 293 | if ! HALCYON_NO_UPLOAD=1 \ 294 | cache_stored_file "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${partial_name}" || 295 | ! partial_hash=$( validate_partial_constraints_file "${partial_file}" ) 296 | then 297 | rm -f "${partial_file}" || true 298 | continue 299 | fi 300 | else 301 | touch_cached_file "${partial_name}" 302 | fi 303 | 304 | local partial_label partial_tag 305 | partial_label=$( format_sandbox_constraints_file_name_label "${partial_name}" ) 306 | partial_tag=$( derive_matching_sandbox_tag "${tag}" "${partial_label}" "${partial_hash}" ) 307 | 308 | echo "${partial_tag}" 309 | done <<<"${partial_names}" || return 0 310 | } 311 | 312 | 313 | score_partial_sandbox_dirs () { 314 | expect_vars HALCYON_CACHE 315 | 316 | local constraints partial_tags 317 | expect_args constraints partial_tags -- "$@" 318 | 319 | if [[ -z "${constraints}" || -z "${partial_tags}" ]]; then 320 | return 0 321 | fi 322 | 323 | local -A packages_A 324 | local package version 325 | packages_A=() 326 | while read -r package version; do 327 | packages_A["${package}"]="${version}" 328 | done <<<"${constraints}" 329 | 330 | log 'Scoring partially-matching sandbox directories' 331 | 332 | local partial_tag 333 | while read -r partial_tag; do 334 | local partial_name partial_file partial_constraints 335 | partial_name=$( format_sandbox_constraints_file_name "${partial_tag}" ) 336 | partial_file="${HALCYON_CACHE}/${partial_name}" 337 | if ! validate_partial_constraints_file "${partial_file}" >'/dev/null' || 338 | ! partial_constraints=$( read_constraints <"${partial_file}" ) || 339 | [[ -z "${partial_constraints}" ]] 340 | then 341 | rm -f "${partial_file}" || true 342 | continue 343 | fi 344 | 345 | local description 346 | description=$( format_sandbox_description "${partial_tag}" ) 347 | 348 | local partial_package partial_version score 349 | score=0 350 | while read -r partial_package partial_version; do 351 | local version 352 | version="${packages_A[${partial_package}]:-}" 353 | if [[ -z "${version}" ]]; then 354 | log_indent "Ignoring ${description} as ${partial_package}-${partial_version} is not needed" 355 | score='' 356 | break 357 | fi 358 | if [[ "${partial_version}" != "${version}" ]]; then 359 | log_indent "Ignoring ${description} as ${partial_package}-${partial_version} is not ${version}" 360 | score='' 361 | break 362 | fi 363 | 364 | score=$(( score + 1 )) 365 | done <<<"${partial_constraints}" 366 | if [[ -n "${score}" ]]; then 367 | local pad 368 | pad=' ' 369 | log_indent "${pad:0:$(( 7 - ${#score} ))}${score}" "${description}" 370 | 371 | if (( score )); then 372 | echo "${score} ${partial_tag}" 373 | fi 374 | fi 375 | done <<<"${partial_tags}" || return 0 376 | } 377 | 378 | 379 | match_sandbox_dir () { 380 | expect_vars HALCYON_NO_BUILD HALCYON_NO_BUILD_DEPENDENCIES \ 381 | HALCYON_INTERNAL_PLATFORM 382 | 383 | local tag constraints 384 | expect_args tag constraints -- "$@" 385 | 386 | local ghc_id constraints_name name_prefix partial_pattern 387 | ghc_id=$( format_ghc_id "${tag}" ) 388 | constraints_name=$( format_sandbox_constraints_file_name "${tag}" ) 389 | name_prefix=$( format_sandbox_common_file_name_prefix ) 390 | partial_pattern=$( format_partial_sandbox_constraints_file_name_pattern "${tag}" ) 391 | 392 | log 'Locating sandbox directories' 393 | 394 | local all_names 395 | all_names=$( 396 | list_stored_files "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}/${name_prefix}" | 397 | sed "s:^${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}/::" | 398 | filter_matching "^${partial_pattern}$" | 399 | filter_not_matching "^${constraints_name}$" | 400 | sort_natural -u | 401 | match_at_least_one 402 | ) || return 1 403 | 404 | local full_tag 405 | if full_tag=$( match_full_sandbox_dir "${tag}" "${all_names}" ); then 406 | echo "${full_tag}" 407 | return 0 408 | fi 409 | 410 | if (( HALCYON_NO_BUILD )) || (( HALCYON_NO_BUILD_DEPENDENCIES )); then 411 | return 1 412 | fi 413 | 414 | local partial_tags result 415 | partial_tags=$( list_partial_sandbox_dirs "${tag}" "${constraints}" "${all_names}" ) 416 | if result=$( 417 | score_partial_sandbox_dirs "${constraints}" "${partial_tags}" | 418 | sort_natural | 419 | filter_last | 420 | match_exactly_one 421 | ); then 422 | echo "${result#* }" 423 | return 0 424 | fi 425 | 426 | return 1 427 | } 428 | -------------------------------------------------------------------------------- /src/install.sh: -------------------------------------------------------------------------------- 1 | create_install_tag () { 2 | local prefix label source_hash \ 3 | ghc_version ghc_magic_hash \ 4 | cabal_version cabal_magic_hash 5 | expect_args prefix label source_hash \ 6 | ghc_version ghc_magic_hash \ 7 | cabal_version cabal_magic_hash -- "$@" 8 | 9 | create_tag "${prefix}" "${label}" "${source_hash}" '' '' \ 10 | "${ghc_version}" "${ghc_magic_hash}" \ 11 | "${cabal_version}" "${cabal_magic_hash}" '' '' \ 12 | '' 13 | } 14 | 15 | 16 | detect_install_tag () { 17 | local tag_file 18 | expect_args tag_file -- "$@" 19 | 20 | local tag_pattern 21 | tag_pattern=$( 22 | create_install_tag '.*' '.*' '.*' \ 23 | '.*' '.*' \ 24 | '.*' '.*' 25 | ) 26 | 27 | local tag 28 | if ! tag=$( detect_tag "${tag_file}" "${tag_pattern}" ); then 29 | log_error 'Failed to detect install tag' 30 | return 1 31 | fi 32 | 33 | echo "${tag}" 34 | } 35 | 36 | 37 | derive_install_tag () { 38 | local tag 39 | expect_args tag -- "$@" 40 | 41 | local prefix label source_hash \ 42 | ghc_version ghc_magic_hash \ 43 | cabal_version cabal_magic_hash 44 | prefix=$( get_tag_prefix "${tag}" ) 45 | label=$( get_tag_label "${tag}" ) 46 | source_hash=$( get_tag_source_hash "${tag}" ) 47 | ghc_version=$( get_tag_ghc_version "${tag}" ) 48 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 49 | cabal_version=$( get_tag_cabal_version "${tag}" ) 50 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 51 | 52 | create_install_tag "${prefix}" "${label}" "${source_hash}" \ 53 | "${ghc_version}" "${ghc_magic_hash}" \ 54 | "${cabal_version}" "${cabal_magic_hash}" 55 | } 56 | 57 | 58 | derive_install_tag_pattern () { 59 | local tag 60 | expect_args tag -- "$@" 61 | 62 | local prefix label source_hash \ 63 | ghc_version ghc_magic_hash \ 64 | cabal_version cabal_magic_hash 65 | prefix=$( get_tag_prefix "${tag}" ) 66 | label=$( get_tag_label "${tag}" ) 67 | source_hash=$( get_tag_source_hash "${tag}" ) 68 | ghc_version=$( get_tag_ghc_version "${tag}" ) 69 | 70 | create_install_tag "${prefix}" "${label//./\.}" "${source_hash}" \ 71 | "${ghc_version//./\.}" ".*" \ 72 | ".*" ".*" 73 | } 74 | 75 | 76 | format_install_id () { 77 | local tag 78 | expect_args tag -- "$@" 79 | 80 | local label source_hash 81 | label=$( get_tag_label "${tag}" ) 82 | source_hash=$( get_tag_source_hash "${tag}" ) 83 | 84 | echo "${source_hash:0:7}-${label}" 85 | } 86 | 87 | 88 | format_install_archive_name () { 89 | local tag 90 | expect_args tag -- "$@" 91 | 92 | local install_id 93 | install_id=$( format_install_id "${tag}" ) 94 | 95 | echo "halcyon-install-${install_id}.tar.gz" 96 | } 97 | 98 | 99 | format_install_archive_name_prefix () { 100 | echo 'halcyon-install-' 101 | } 102 | 103 | 104 | format_install_archive_name_pattern () { 105 | local tag 106 | expect_args tag -- "$@" 107 | 108 | local label 109 | label=$( get_tag_label "${tag}" ) 110 | 111 | echo "halcyon-install-.*-${label//./\.}.tar.gz" 112 | } 113 | 114 | 115 | install_extra_apps () { 116 | local tag source_dir install_dir 117 | expect_args tag source_dir install_dir -- "$@" 118 | 119 | if [[ ! -f "${source_dir}/.halcyon/extra-apps" ]]; then 120 | return 0 121 | fi 122 | 123 | local prefix 124 | prefix=$( get_tag_prefix "${tag}" ) 125 | 126 | local ghc_version ghc_magic_hash 127 | ghc_version=$( get_tag_ghc_version "${tag}" ) 128 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 129 | 130 | local cabal_version cabal_magic_hash cabal_remote_repo 131 | cabal_version=$( get_tag_cabal_version "${tag}" ) 132 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 133 | cabal_remote_repo=$( get_tag_cabal_remote_repo "${tag}" ) 134 | 135 | local extra_constraints 136 | extra_constraints="${source_dir}/.halcyon/extra-apps-constraints" 137 | 138 | local -a opts_a 139 | opts_a=() 140 | opts_a+=( --prefix="${prefix}" ) 141 | opts_a+=( --root="${install_dir}" ) 142 | opts_a+=( --ghc-version="${ghc_version}" ) 143 | opts_a+=( --cabal-version="${cabal_version}" ) 144 | opts_a+=( --cabal-remote-repo="${cabal_remote_repo}" ) 145 | [[ -e "${extra_constraints}" ]] && opts_a+=( --constraints="${extra_constraints}" ) 146 | 147 | log 'Installing extra apps' 148 | 149 | local extra_app index 150 | index=0 151 | while read -r extra_app; do 152 | local thing 153 | if [[ -d "${source_dir}/${extra_app}" ]]; then 154 | thing="${source_dir}/${extra_app}" 155 | else 156 | thing="${extra_app}" 157 | fi 158 | 159 | index=$(( index + 1 )) 160 | if (( index > 1 )); then 161 | log 162 | fi 163 | 164 | # NOTE: Returns 2 if build is needed. 165 | HALCYON_INTERNAL_RECURSIVE=1 \ 166 | HALCYON_INTERNAL_GHC_MAGIC_HASH="${ghc_magic_hash}" \ 167 | HALCYON_INTERNAL_CABAL_MAGIC_HASH="${cabal_magic_hash}" \ 168 | HALCYON_INTERNAL_NO_COPY_LOCAL_SOURCE=1 \ 169 | halcyon install "${opts_a[@]}" "${thing}" 2>&1 | quote || return 170 | done <"${source_dir}/.halcyon/extra-apps" || return 0 171 | } 172 | 173 | 174 | install_extra_data_files () { 175 | expect_vars HALCYON_BASE 176 | 177 | local tag source_dir build_dir install_dir 178 | expect_args tag source_dir build_dir install_dir -- "$@" 179 | 180 | if [[ ! -f "${source_dir}/.halcyon/extra-data-files" ]]; then 181 | return 0 182 | fi 183 | 184 | expect_existing "${build_dir}/dist/.halcyon-data-dir" || return 1 185 | 186 | local data_dir 187 | data_dir=$( <"${build_dir}/dist/.halcyon-data-dir" ) || true 188 | if [[ -z "${data_dir}" ]]; then 189 | log_error 'Failed to read data directory file' 190 | return 1 191 | fi 192 | 193 | log_indent 'Installing extra data files' 194 | 195 | local glob 196 | while read -r glob; do 197 | copy_dir_glob_into "${build_dir}" "${glob}" "${install_dir}${data_dir}" || return 1 198 | done <"${source_dir}/.halcyon/extra-data-files" || return 0 199 | } 200 | 201 | 202 | install_extra_os_packages () { 203 | local tag source_dir install_dir 204 | expect_args tag source_dir install_dir -- "$@" 205 | 206 | if [[ ! -f "${source_dir}/.halcyon/extra-os-packages" ]]; then 207 | return 0 208 | fi 209 | 210 | local extra_packages prefix 211 | extra_packages=$( <"${source_dir}/.halcyon/extra-os-packages" ) || true 212 | prefix=$( get_tag_prefix "${tag}" ) 213 | 214 | log 'Installing extra OS packages' 215 | 216 | if ! install_platform_packages "${extra_packages}" "${install_dir}${prefix}"; then 217 | log_error 'Failed to install extra OS packages' 218 | return 1 219 | fi 220 | } 221 | 222 | 223 | prepare_install_dir () { 224 | expect_vars HALCYON_BASE \ 225 | HALCYON_APP_NO_REMOVE_DOC 226 | 227 | local tag source_dir constraints build_dir install_dir 228 | expect_args tag source_dir constraints build_dir install_dir -- "$@" 229 | 230 | expect_existing "${build_dir}/.halcyon-tag" "${build_dir}/dist/.halcyon-data-dir" || return 1 231 | 232 | local data_dir 233 | data_dir=$( <"${build_dir}/dist/.halcyon-data-dir" ) || true 234 | if [[ -z "${data_dir}" ]]; then 235 | log_error 'Failed to read data directory file' 236 | return 1 237 | fi 238 | 239 | local prefix label ghc_version cabal_version label_dir 240 | prefix=$( get_tag_prefix "${tag}" ) 241 | label=$( get_tag_label "${tag}" ) 242 | ghc_version=$( get_tag_ghc_version "${tag}" ) 243 | cabal_version=$( get_tag_cabal_version "${tag}" ) 244 | label_dir="${install_dir}${prefix}/.halcyon/${label}" 245 | 246 | local -a copy_opts_a register_opts_a 247 | copy_opts_a=() 248 | copy_opts_a+=( --destdir="${install_dir}" ) 249 | copy_opts_a+=( --verbose=0 ) 250 | register_opts_a=() 251 | register_opts_a+=( --gen-pkg-config="${label_dir}/${label}.conf" ) 252 | register_opts_a+=( --verbose=0 ) 253 | 254 | log 'Preparing install directory' 255 | 256 | # NOTE: PATH is extended to silence a misleading Cabal warning. 257 | if ! PATH="${install_dir}${prefix}:${PATH}" \ 258 | sandboxed_cabal_do "${build_dir}" copy "${copy_opts_a[@]}" 2>&1 | quote || 259 | ! mkdir -p "${label_dir}" || 260 | ! sandboxed_cabal_do "${build_dir}" register "${register_opts_a[@]}" 2>&1 | quote 261 | then 262 | log_error 'Failed to copy app to install directory' 263 | return 1 264 | fi 265 | 266 | # NOTE: Returns 2 if build is needed. 267 | install_extra_apps "${tag}" "${source_dir}" "${install_dir}" || return 268 | 269 | if ! install_extra_data_files "${tag}" "${source_dir}" "${build_dir}" "${install_dir}"; then 270 | log_error 'Failed to install extra data files' 271 | return 1 272 | fi 273 | 274 | # NOTE: Cabal libraries may require data files at run-time. 275 | # See filestore for an example. 276 | # https://haskell.org/cabal/users-guide/developing-packages.html#accessing-data-files-from-package-code 277 | if find_tree "${HALCYON_BASE}/sandbox/share" -type f | 278 | match_at_least_one >'/dev/null' 279 | then 280 | log 'Installing extra data files for dependencies' 281 | 282 | copy_dir_into "${HALCYON_BASE}/sandbox/share" "${install_dir}${HALCYON_BASE}/sandbox/share" || return 1 283 | fi 284 | 285 | install_extra_os_packages "${tag}" "${source_dir}" "${install_dir}" || return 1 286 | 287 | if [[ -f "${source_dir}/.halcyon/pre-install-hook" ]]; then 288 | log 'Executing pre-install hook' 289 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 290 | HALCYON_GHC_VERSION="${ghc_version}" \ 291 | HALCYON_CABAL_VERSION="${cabal_version}" \ 292 | "${source_dir}/.halcyon/pre-install-hook" \ 293 | "${tag}" "${source_dir}" "${install_dir}" "${data_dir}" 2>&1 | quote 294 | then 295 | log_error 'Failed to execute pre-install hook' 296 | return 1 297 | fi 298 | log 'Pre-install hook executed' 299 | fi 300 | 301 | local executable 302 | if executable=$( detect_executable "${source_dir}" ); then 303 | if ! echo "${executable}" >"${label_dir}/executable"; then 304 | log_error 'Failed to prepare install directory' 305 | return 1 306 | fi 307 | fi 308 | 309 | local prepared_size 310 | if ! format_constraints <<<"${constraints}" >"${label_dir}/constraints" || 311 | ! echo "${data_dir}" >"${label_dir}/data-dir" || 312 | ! derive_install_tag "${tag}" >"${label_dir}/tag" || 313 | ! prepared_size=$( get_size "${install_dir}" ) 314 | then 315 | log_error 'Failed to prepare install directory' 316 | return 1 317 | fi 318 | log "Install directory prepared, ${prepared_size}" 319 | 320 | if ! (( HALCYON_APP_NO_REMOVE_DOC )) && 321 | [[ -d "${install_dir}${prefix}/share/doc" ]] 322 | then 323 | log_indent_begin 'Removing documentation from install directory...' 324 | 325 | local trimmed_size 326 | if ! rm -rf "${install_dir}${prefix}/share/doc" || 327 | ! trimmed_size=$( get_size "${install_dir}" ) 328 | then 329 | log_indent_end 'error' 330 | return 1 331 | fi 332 | log_indent_end "done, ${trimmed_size}" 333 | fi 334 | 335 | if ! derive_install_tag "${tag}" >"${install_dir}/.halcyon-tag"; then 336 | log_error 'Failed to write install tag' 337 | return 1 338 | fi 339 | } 340 | 341 | 342 | archive_install_dir () { 343 | expect_vars HALCYON_NO_ARCHIVE \ 344 | HALCYON_INTERNAL_PLATFORM 345 | 346 | local install_dir 347 | expect_args install_dir -- "$@" 348 | 349 | if (( HALCYON_NO_ARCHIVE )); then 350 | return 0 351 | fi 352 | 353 | expect_existing "${install_dir}/.halcyon-tag" || return 1 354 | 355 | local install_tag ghc_id archive_name 356 | install_tag=$( detect_install_tag "${install_dir}/.halcyon-tag" ) || return 1 357 | ghc_id=$( format_ghc_id "${install_tag}" ) 358 | archive_name=$( format_install_archive_name "${install_tag}" ) 359 | 360 | log 'Archiving install directory' 361 | 362 | create_cached_archive "${install_dir}" "${archive_name}" || return 1 363 | upload_cached_file "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${archive_name}" || return 1 364 | 365 | local archive_prefix archive_pattern 366 | archive_prefix=$( format_install_archive_name_prefix ) 367 | archive_pattern=$( format_install_archive_name_pattern "${install_tag}" ) 368 | 369 | delete_matching_private_stored_files "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${archive_prefix}" "${archive_pattern}" "${archive_name}" || return 1 370 | } 371 | 372 | 373 | validate_install_dir () { 374 | local tag install_dir 375 | expect_args tag install_dir -- "$@" 376 | 377 | local install_pattern 378 | install_pattern=$( derive_install_tag_pattern "${tag}" ) 379 | detect_tag "${install_dir}/.halcyon-tag" "${install_pattern}" || return 1 380 | } 381 | 382 | 383 | restore_install_dir () { 384 | expect_vars HALCYON_INTERNAL_PLATFORM 385 | 386 | local tag install_dir 387 | expect_args tag install_dir -- "$@" 388 | 389 | local ghc_id archive_name archive_pattern 390 | ghc_id=$( format_ghc_id "${tag}" ) 391 | archive_name=$( format_install_archive_name "${tag}" ) 392 | archive_pattern=$( format_install_archive_name_pattern "${tag}" ) 393 | 394 | log 'Restoring install directory' 395 | 396 | if ! extract_cached_archive_over "${archive_name}" "${install_dir}" || 397 | ! validate_install_dir "${tag}" "${install_dir}" >'/dev/null' 398 | then 399 | rm -rf "${install_dir}" || true 400 | cache_stored_file "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${archive_name}" || return 1 401 | 402 | if ! extract_cached_archive_over "${archive_name}" "${install_dir}" || 403 | ! validate_install_dir "${tag}" "${install_dir}" >'/dev/null' 404 | then 405 | rm -rf "${install_dir}" || true 406 | 407 | log_warning 'Failed to restore install directory' 408 | return 1 409 | fi 410 | else 411 | touch_cached_file "${archive_name}" 412 | fi 413 | } 414 | 415 | 416 | install_app () { 417 | expect_vars HALCYON_BASE HALCYON_ROOT \ 418 | HALCYON_KEEP_DEPENDENCIES \ 419 | HALCYON_INTERNAL_RECURSIVE 420 | 421 | local tag source_dir install_dir 422 | expect_args tag source_dir install_dir -- "$@" 423 | 424 | local prefix label label_dir 425 | prefix=$( get_tag_prefix "${tag}" ) 426 | label=$( get_tag_label "${tag}" ) 427 | label_dir="${install_dir}${prefix}/.halcyon/${label}" 428 | 429 | expect_existing "${label_dir}/data-dir" || return 1 430 | 431 | local data_dir 432 | data_dir=$( <"${label_dir}/data-dir" ) || true 433 | if [[ -z "${data_dir}" ]]; then 434 | log_error 'Failed to read data directory file' 435 | return 1 436 | fi 437 | 438 | log "Installing app to ${prefix}" 439 | 440 | if ! copy_dir_into "${install_dir}${prefix}" "${HALCYON_ROOT}${prefix}"; then 441 | log_error 'Failed to copy app to root directory' 442 | return 1 443 | fi 444 | 445 | if (( HALCYON_KEEP_DEPENDENCIES )); then 446 | if ! ln -fs "${HALCYON_BASE}/sandbox/cabal.sandbox.config" \ 447 | "${HALCYON_ROOT}${prefix}/cabal.sandbox.config" 448 | then 449 | log_error 'Failed to symlink sandbox config' 450 | return 1 451 | fi 452 | fi 453 | 454 | if [[ -f "${source_dir}/.halcyon/post-install-hook" ]]; then 455 | log 'Executing post-install hook' 456 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 457 | HALCYON_GHC_VERSION="${ghc_version}" \ 458 | HALCYON_CABAL_VERSION="${cabal_version}" \ 459 | "${source_dir}/.halcyon/post-install-hook" \ 460 | "${tag}" "${source_dir}" "${install_dir}" "${data_dir}" 2>&1 | quote 461 | then 462 | log_error 'Failed to execute post-install hook' 463 | return 1 464 | fi 465 | log 'Post-install hook executed' 466 | fi 467 | 468 | log "Installed ${label}" 469 | } 470 | -------------------------------------------------------------------------------- /src/build.sh: -------------------------------------------------------------------------------- 1 | create_build_tag () { 2 | local prefix label source_hash constraints_hash magic_hash \ 3 | ghc_version ghc_magic_hash \ 4 | cabal_version cabal_magic_hash \ 5 | sandbox_magic_hash 6 | expect_args prefix label source_hash constraints_hash magic_hash \ 7 | ghc_version ghc_magic_hash \ 8 | cabal_version cabal_magic_hash \ 9 | sandbox_magic_hash -- "$@" 10 | 11 | create_tag "${prefix}" "${label}" "${source_hash}" "${constraints_hash}" "${magic_hash}" \ 12 | "${ghc_version}" "${ghc_magic_hash}" \ 13 | "${cabal_version}" "${cabal_magic_hash}" '' '' \ 14 | "${sandbox_magic_hash}" 15 | } 16 | 17 | 18 | detect_build_tag () { 19 | local tag_file 20 | expect_args tag_file -- "$@" 21 | 22 | local tag_pattern 23 | tag_pattern=$( 24 | create_build_tag '.*' '.*' '.*' '.*' '.*' \ 25 | '.*' '.*' \ 26 | '.*' '.*' \ 27 | '.*' 28 | ) 29 | 30 | local tag 31 | if ! tag=$( detect_tag "${tag_file}" "${tag_pattern}" ); then 32 | log_error 'Failed to detect build tag' 33 | return 1 34 | fi 35 | 36 | echo "${tag}" 37 | } 38 | 39 | 40 | derive_build_tag () { 41 | local tag 42 | expect_args tag -- "$@" 43 | 44 | local prefix label source_hash constraints_hash magic_hash \ 45 | ghc_version ghc_magic_hash \ 46 | cabal_version cabal_magic_hash \ 47 | sandbox_magic_hash 48 | prefix=$( get_tag_prefix "${tag}" ) 49 | label=$( get_tag_label "${tag}" ) 50 | source_hash=$( get_tag_source_hash "${tag}" ) 51 | constraints_hash=$( get_tag_constraints_hash "${tag}" ) 52 | magic_hash=$( get_tag_magic_hash "${tag}" ) 53 | ghc_version=$( get_tag_ghc_version "${tag}" ) 54 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 55 | cabal_version=$( get_tag_cabal_version "${tag}" ) 56 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 57 | sandbox_magic_hash=$( get_tag_sandbox_magic_hash "${tag}" ) 58 | 59 | create_build_tag "${prefix}" "${label}" "${source_hash}" "${constraints_hash}" "${magic_hash}" \ 60 | "${ghc_version}" "${ghc_magic_hash}" \ 61 | "${cabal_version}" "${cabal_magic_hash}" \ 62 | "${sandbox_magic_hash}" 63 | } 64 | 65 | 66 | derive_configured_build_tag_pattern () { 67 | local tag 68 | expect_args tag -- "$@" 69 | 70 | local prefix label constraints_hash magic_hash \ 71 | ghc_version ghc_magic_hash \ 72 | cabal_version cabal_magic_hash \ 73 | sandbox_magic_hash 74 | prefix=$( get_tag_prefix "${tag}" ) 75 | label=$( get_tag_label "${tag}" ) 76 | constraints_hash=$( get_tag_constraints_hash "${tag}" ) 77 | magic_hash=$( get_tag_magic_hash "${tag}" ) 78 | ghc_version=$( get_tag_ghc_version "${tag}" ) 79 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 80 | cabal_version=$( get_tag_cabal_version "${tag}" ) 81 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 82 | sandbox_magic_hash=$( get_tag_sandbox_magic_hash "${tag}" ) 83 | 84 | create_build_tag "${prefix}" "${label//./\.}" '.*' "${constraints_hash}" '.*' \ 85 | "${ghc_version//./\.}" "${ghc_magic_hash}" \ 86 | "${cabal_version//./\.}" "${cabal_magic_hash}" \ 87 | "${sandbox_magic_hash}" 88 | } 89 | 90 | 91 | derive_potential_build_tag_pattern () { 92 | local tag 93 | expect_args tag -- "$@" 94 | 95 | local label \ 96 | ghc_version ghc_magic_hash \ 97 | cabal_version cabal_magic_hash 98 | label=$( get_tag_label "${tag}" ) 99 | ghc_version=$( get_tag_ghc_version "${tag}" ) 100 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 101 | cabal_version=$( get_tag_cabal_version "${tag}" ) 102 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 103 | 104 | # NOTE: Build directories created with cabal-install 1.20.0.3 are 105 | # not usable with cabal-install 1.22.0.0. 106 | # https://github.com/haskell/cabal/issues/2320 107 | create_build_tag '.*' "${label//./\.}" '.*' '.*' '.*' \ 108 | "${ghc_version//./\.}" "${ghc_magic_hash}" \ 109 | "${cabal_version//./\.}" "${cabal_magic_hash}" \ 110 | '.*' 111 | } 112 | 113 | 114 | format_build_archive_name () { 115 | local tag 116 | expect_args tag -- "$@" 117 | 118 | local label 119 | label=$( get_tag_label "${tag}" ) 120 | 121 | echo "halcyon-build-${label}.tar.gz" 122 | } 123 | 124 | 125 | do_build_app () { 126 | expect_vars HALCYON_BASE \ 127 | HALCYON_APP_NO_STRIP 128 | 129 | local tag must_copy must_configure source_dir build_dir 130 | expect_args tag must_copy must_configure source_dir build_dir -- "$@" 131 | 132 | expect_existing "${source_dir}" || return 1 133 | if (( must_copy )); then 134 | copy_dir_over "${source_dir}" "${build_dir}" || return 1 135 | else 136 | expect_existing "${build_dir}/.halcyon-tag" || return 1 137 | fi 138 | 139 | local prefix ghc_version cabal_version 140 | prefix=$( get_tag_prefix "${tag}" ) 141 | ghc_version=$( get_tag_ghc_version "${tag}" ) 142 | cabal_version=$( get_tag_cabal_version "${tag}" ) 143 | 144 | if (( must_copy )) || (( must_configure )); then 145 | log 'Configuring app' 146 | 147 | local -a opts_a 148 | opts_a=() 149 | if [[ -f "${source_dir}/.halcyon/extra-configure-flags" ]]; then 150 | opts_a=( $( <"${source_dir}/.halcyon/extra-configure-flags" ) ) || true 151 | fi 152 | opts_a+=( --prefix="${prefix}" ) 153 | opts_a+=( --verbose ) 154 | 155 | local stdout 156 | stdout=$( get_tmp_file 'cabal-configure.stdout' ) || return 1 157 | 158 | if ! sandboxed_cabal_do "${build_dir}" configure "${opts_a[@]}" >"${stdout}" 2>&1 | quote; then 159 | quote <"${stdout}" 160 | 161 | log_error 'Failed to configure app' 162 | return 1 163 | fi 164 | 165 | # NOTE: Storing the data dir helps implement 166 | # HALCYON_EXTRA_DATA_FILES, which works around unusual Cabal 167 | # globbing for the data-files package description entry. 168 | # https://github.com/haskell/cabal/issues/713 169 | # https://github.com/haskell/cabal/issues/784 170 | local data_dir 171 | if ! data_dir=$( 172 | awk ' /Data files installed in:/ { i = 1 } 173 | /Documentation installed in:/ { i = 0 } 174 | i' <"${stdout}" | 175 | strip_trailing_newline | 176 | tr '\n' ' ' | 177 | sed 's/^Data files installed in: //' 178 | ) || 179 | ! echo "${data_dir}" >"${build_dir}/dist/.halcyon-data-dir" 180 | then 181 | log_error 'Failed to write data directory file' 182 | return 1 183 | fi 184 | else 185 | expect_existing "${build_dir}/dist/.halcyon-data-dir" || return 1 186 | fi 187 | 188 | if [[ -f "${source_dir}/.halcyon/pre-build-hook" ]]; then 189 | log 'Executing pre-build hook' 190 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 191 | HALCYON_GHC_VERSION="${ghc_version}" \ 192 | HALCYON_CABAL_VERSION="${cabal_version}" \ 193 | "${source_dir}/.halcyon/pre-build-hook" \ 194 | "${tag}" "${source_dir}" "${build_dir}" 2>&1 | quote 195 | then 196 | log_error 'Failed to execute pre-build hook' 197 | return 1 198 | fi 199 | log 'Pre-build hook executed' 200 | fi 201 | 202 | log 'Building app' 203 | 204 | local built_size 205 | if ! sandboxed_cabal_do "${build_dir}" build 2>&1 | quote || 206 | ! built_size=$( get_size "${build_dir}" ) 207 | then 208 | log_error 'Failed to build app' 209 | return 1 210 | fi 211 | log "App built, ${built_size}" 212 | 213 | if [[ -f "${source_dir}/.halcyon/post-build-hook" ]]; then 214 | log 'Executing post-build hook' 215 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 216 | HALCYON_GHC_VERSION="${ghc_version}" \ 217 | HALCYON_CABAL_VERSION="${cabal_version}" \ 218 | "${source_dir}/.halcyon/post-build-hook" \ 219 | "${tag}" "${source_dir}" "${build_dir}" 2>&1 | quote 220 | then 221 | log_error 'Failed to execute post-build hook' 222 | return 1 223 | fi 224 | log 'Post-build hook executed' 225 | fi 226 | 227 | if ! (( HALCYON_APP_NO_STRIP )); then 228 | log_indent_begin 'Stripping app...' 229 | 230 | local stripped_size 231 | if ! strip_tree "${build_dir}" || 232 | ! stripped_size=$( get_size "${build_dir}" ) 233 | then 234 | log_indent_end 'error' 235 | return 1 236 | fi 237 | log_indent_end "done, ${stripped_size}" 238 | fi 239 | 240 | if ! derive_build_tag "${tag}" >"${build_dir}/.halcyon-tag"; then 241 | log_error 'Failed to write build tag' 242 | return 1 243 | fi 244 | } 245 | 246 | 247 | archive_build_dir () { 248 | expect_vars HALCYON_NO_ARCHIVE \ 249 | HALCYON_INTERNAL_PLATFORM 250 | 251 | local build_dir 252 | expect_args build_dir -- "$@" 253 | 254 | if (( HALCYON_NO_ARCHIVE )); then 255 | return 0 256 | fi 257 | 258 | expect_existing "${build_dir}/.halcyon-tag" || return 1 259 | 260 | local build_tag ghc_id archive_name 261 | build_tag=$( detect_build_tag "${build_dir}/.halcyon-tag" ) || return 1 262 | ghc_id=$( format_ghc_id "${build_tag}" ) 263 | archive_name=$( format_build_archive_name "${build_tag}" ) 264 | 265 | log 'Archiving build directory' 266 | 267 | create_cached_archive "${build_dir}" "${archive_name}" || return 1 268 | upload_cached_file "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${archive_name}" || return 1 269 | } 270 | 271 | 272 | validate_potential_build_dir () { 273 | local tag build_dir 274 | expect_args tag build_dir -- "$@" 275 | 276 | local potential_pattern 277 | potential_pattern=$( derive_potential_build_tag_pattern "${tag}" ) 278 | detect_tag "${build_dir}/.halcyon-tag" "${potential_pattern}" || return 1 279 | } 280 | 281 | 282 | validate_configured_build_dir () { 283 | local tag build_dir 284 | expect_args tag build_dir -- "$@" 285 | 286 | local configured_pattern 287 | configured_pattern=$( derive_configured_build_tag_pattern "${tag}" ) 288 | detect_tag "${build_dir}/.halcyon-tag" "${configured_pattern}" || return 1 289 | 290 | if [[ ! -f "${build_dir}/dist/setup-config" ]]; then 291 | return 1 292 | fi 293 | } 294 | 295 | 296 | validate_build_dir () { 297 | local tag build_dir 298 | expect_args tag build_dir -- "$@" 299 | 300 | local build_tag 301 | build_tag=$( derive_build_tag "${tag}" ) 302 | detect_tag "${build_dir}/.halcyon-tag" "${build_tag//./\.}" || return 1 303 | 304 | if [[ ! -f "${build_dir}/dist/setup-config" ]]; then 305 | return 1 306 | fi 307 | } 308 | 309 | 310 | restore_build_dir () { 311 | expect_vars HALCYON_INTERNAL_PLATFORM 312 | 313 | local tag build_dir 314 | expect_args tag build_dir -- "$@" 315 | 316 | local ghc_id archive_name 317 | ghc_id=$( format_ghc_id "${tag}" ) 318 | archive_name=$( format_build_archive_name "${tag}" ) 319 | 320 | log 'Restoring build directory' 321 | 322 | if ! extract_cached_archive_over "${archive_name}" "${build_dir}" || 323 | ! validate_potential_build_dir "${tag}" "${build_dir}" >'/dev/null' 324 | then 325 | rm -rf "${build_dir}" || true 326 | cache_stored_file "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${archive_name}" || return 1 327 | 328 | if ! extract_cached_archive_over "${archive_name}" "${build_dir}" || 329 | ! validate_potential_build_dir "${tag}" "${build_dir}" >'/dev/null' 330 | then 331 | rm -rf "${build_dir}" || true 332 | 333 | log_warning 'Failed to restore build directory' 334 | return 1 335 | fi 336 | else 337 | touch_cached_file "${archive_name}" 338 | fi 339 | } 340 | 341 | 342 | prepare_build_dir () { 343 | local tag source_dir build_dir 344 | expect_args tag source_dir build_dir -- "$@" 345 | 346 | expect_existing "${source_dir}" "${build_dir}/.halcyon-tag" \ 347 | "${build_dir}/dist/setup-config" || return 1 348 | 349 | local -a opts_a 350 | opts_a=() 351 | # NOTE: Ignoring the same files as in hash_source. 352 | opts_a+=( \( -name '.git' ) 353 | opts_a+=( -o -name '.gitmodules' ) 354 | opts_a+=( -o -name '.ghc' ) 355 | opts_a+=( -o -name '.cabal' ) 356 | opts_a+=( -o -name '.cabal-sandbox' ) 357 | opts_a+=( -o -name 'cabal.sandbox.config' ) 358 | if [[ -f "${source_dir}/.halcyon/extra-source-hash-ignore" ]]; then 359 | local ignore 360 | while read -r ignore; do 361 | opts_a+=( -o -name "${ignore}" ) 362 | done <"${source_dir}/.halcyon/extra-source-hash-ignore" 363 | fi 364 | # NOTE: Ignoring files expected in build dir only, even though 365 | # they may also be in source dir. 366 | opts_a+=( -o -name '.halcyon-tag' ) 367 | opts_a+=( -o -name 'dist' ) 368 | opts_a+=( \) -prune -o ) 369 | 370 | local all_files 371 | all_files=$( compare_tree "${build_dir}" "${source_dir}" "${opts_a[@]}" ) 372 | 373 | local changed_files 374 | if ! changed_files=$( 375 | filter_not_matching '^= ' <<<"${all_files}" | 376 | match_at_least_one 377 | ); then 378 | return 0 379 | fi 380 | 381 | log 'Examining source changes' 382 | 383 | quote <<<"${changed_files}" 384 | 385 | local label prepare_dir 386 | label=$( get_tag_label "${tag}" ) 387 | prepare_dir=$( get_tmp_dir "prepare-${label}" ) || return 1 388 | 389 | copy_dir_over "${source_dir}" "${prepare_dir}" || return 1 390 | 391 | # NOTE: Restoring file modification times of unchanged files is 392 | # necessary to avoid needless recompilation. 393 | local file 394 | filter_matching '^= ' <<<"${all_files}" | 395 | while read -r file; do 396 | touch -r "${build_dir}/${file#= }" "${prepare_dir}/${file#= }" || true 397 | done 398 | 399 | # NOTE: Any build products outside dist will have to be rebuilt. 400 | # See alex or happy for examples. 401 | rm -rf "${prepare_dir}/dist" || return 1 402 | mv "${build_dir}/.halcyon-tag" "${prepare_dir}/.halcyon-tag" || return 1 403 | mv "${build_dir}/dist" "${prepare_dir}/dist" || return 1 404 | rm -rf "${build_dir}" || return 1 405 | mv "${prepare_dir}" "${build_dir}" || return 1 406 | 407 | # NOTE: With build-type: Custom, changing Setup.hs requires manually 408 | # re-running configure, as Cabal fails to detect the change. 409 | # Detecting changes in cabal.config works around a Cabal issue. 410 | # https://github.com/mietek/haskell-on-heroku/issues/29 411 | # https://github.com/haskell/cabal/issues/1992 412 | if filter_matching "^. (\.halcyon/extra-configure-flags|cabal\.config|Setup\.hs|.*\.cabal)$" <<<"${changed_files}" | 413 | match_at_least_one >'/dev/null' 414 | then 415 | rm -f "${build_dir}/dist/setup-config" || true 416 | fi 417 | } 418 | 419 | 420 | build_app () { 421 | expect_vars HALCYON_NO_BUILD \ 422 | HALCYON_APP_REBUILD HALCYON_APP_RECONFIGURE \ 423 | HALCYON_SANDBOX_REBUILD 424 | 425 | local tag source_dir build_dir 426 | expect_args tag source_dir build_dir -- "$@" 427 | 428 | # NOTE: Returns 2 if build is needed. 429 | if (( HALCYON_NO_BUILD )); then 430 | log_error 'Cannot build app' 431 | return 2 432 | fi 433 | 434 | if ! (( HALCYON_APP_REBUILD )) && ! (( HALCYON_SANDBOX_REBUILD )) && 435 | restore_build_dir "${tag}" "${build_dir}" 436 | then 437 | if ! (( HALCYON_APP_RECONFIGURE )) && 438 | validate_build_dir "${tag}" "${build_dir}" >'/dev/null' 439 | then 440 | return 0 441 | fi 442 | 443 | local must_copy must_configure 444 | must_copy=0 445 | must_configure="${HALCYON_APP_RECONFIGURE}" 446 | if ! prepare_build_dir "${tag}" "${source_dir}" "${build_dir}"; then 447 | must_copy=1 448 | elif ! validate_configured_build_dir "${tag}" "${build_dir}" >'/dev/null'; then 449 | must_configure=1 450 | fi 451 | do_build_app "${tag}" "${must_copy}" "${must_configure}" "${source_dir}" "${build_dir}" || return 1 452 | archive_build_dir "${build_dir}" || return 1 453 | return 0 454 | fi 455 | 456 | local must_copy must_configure 457 | must_copy=1 458 | must_configure=1 459 | do_build_app "${tag}" "${must_copy}" "${must_configure}" "${source_dir}" "${build_dir}" || return 1 460 | archive_build_dir "${build_dir}" || return 1 461 | } 462 | -------------------------------------------------------------------------------- /src/sandbox.sh: -------------------------------------------------------------------------------- 1 | create_sandbox_tag () { 2 | local label constraints_hash \ 3 | ghc_version ghc_magic_hash \ 4 | cabal_version cabal_magic_hash \ 5 | sandbox_magic_hash 6 | expect_args label constraints_hash \ 7 | ghc_version ghc_magic_hash \ 8 | cabal_version cabal_magic_hash \ 9 | sandbox_magic_hash -- "$@" 10 | 11 | create_tag '' "${label}" '' "${constraints_hash}" '' \ 12 | "${ghc_version}" "${ghc_magic_hash}" \ 13 | "${cabal_version}" "${cabal_magic_hash}" '' '' \ 14 | "${sandbox_magic_hash}" 15 | } 16 | 17 | 18 | detect_sandbox_tag () { 19 | local tag_file 20 | expect_args tag_file -- "$@" 21 | 22 | local tag_pattern 23 | tag_pattern=$( 24 | create_sandbox_tag '.*' '.*' \ 25 | '.*' '.*' \ 26 | '.*' '.*' \ 27 | '.*' 28 | ) 29 | 30 | local tag 31 | if ! tag=$( detect_tag "${tag_file}" "${tag_pattern}" ); then 32 | log_error 'Failed to detect sandbox tag' 33 | return 1 34 | fi 35 | 36 | echo "${tag}" 37 | } 38 | 39 | 40 | derive_sandbox_tag () { 41 | local tag 42 | expect_args tag -- "$@" 43 | 44 | local label constraints_hash \ 45 | ghc_version ghc_magic_hash \ 46 | cabal_version cabal_magic_hash \ 47 | sandbox_magic_hash 48 | label=$( get_tag_label "${tag}" ) 49 | constraints_hash=$( get_tag_constraints_hash "${tag}" ) 50 | ghc_version=$( get_tag_ghc_version "${tag}" ) 51 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 52 | cabal_version=$( get_tag_cabal_version "${tag}" ) 53 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 54 | sandbox_magic_hash=$( get_tag_sandbox_magic_hash "${tag}" ) 55 | 56 | create_sandbox_tag "${label}" "${constraints_hash}" \ 57 | "${ghc_version}" "${ghc_magic_hash}" \ 58 | "${cabal_version}" "${cabal_magic_hash}" \ 59 | "${sandbox_magic_hash}" 60 | } 61 | 62 | 63 | derive_matching_sandbox_tag () { 64 | local tag label constraints_hash 65 | expect_args tag label constraints_hash -- "$@" 66 | 67 | local ghc_version ghc_magic_hash \ 68 | cabal_version cabal_magic_hash \ 69 | sandbox_magic_hash 70 | ghc_version=$( get_tag_ghc_version "${tag}" ) 71 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 72 | cabal_version=$( get_tag_cabal_version "${tag}" ) 73 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 74 | sandbox_magic_hash=$( get_tag_sandbox_magic_hash "${tag}" ) 75 | 76 | create_sandbox_tag "${label}" "${constraints_hash}" \ 77 | "${ghc_version}" "${ghc_magic_hash}" \ 78 | "${cabal_version}" "${cabal_magic_hash}" \ 79 | "${sandbox_magic_hash}" 80 | } 81 | 82 | 83 | format_sandbox_id () { 84 | local tag 85 | expect_args tag -- "$@" 86 | 87 | local constraints_hash sandbox_magic_hash 88 | constraints_hash=$( get_tag_constraints_hash "${tag}" ) 89 | sandbox_magic_hash=$( get_tag_sandbox_magic_hash "${tag}" ) 90 | 91 | echo "${constraints_hash:0:7}${sandbox_magic_hash:+.${sandbox_magic_hash:0:7}}" 92 | } 93 | 94 | 95 | format_sandbox_description () { 96 | local tag 97 | expect_args tag -- "$@" 98 | 99 | local label sandbox_id 100 | label=$( get_tag_label "${tag}" ) 101 | sandbox_id=$( format_sandbox_id "${tag}" ) 102 | 103 | echo "${label} (${sandbox_id})" 104 | } 105 | 106 | 107 | format_sandbox_archive_name () { 108 | local tag 109 | expect_args tag -- "$@" 110 | 111 | local label sandbox_id 112 | label=$( get_tag_label "${tag}" ) 113 | sandbox_id=$( format_sandbox_id "${tag}" ) 114 | 115 | echo "halcyon-sandbox-${sandbox_id}-${label}.tar.gz" 116 | } 117 | 118 | 119 | format_sandbox_constraints_file_name () { 120 | local tag 121 | expect_args tag -- "$@" 122 | 123 | local label sandbox_id 124 | label=$( get_tag_label "${tag}" ) 125 | sandbox_id=$( format_sandbox_id "${tag}" ) 126 | 127 | echo "halcyon-sandbox-${sandbox_id}-${label}.constraints" 128 | } 129 | 130 | 131 | format_full_sandbox_constraints_file_name_pattern () { 132 | local tag 133 | expect_args tag -- "$@" 134 | 135 | local sandbox_id 136 | sandbox_id=$( format_sandbox_id "${tag}" ) 137 | 138 | echo "halcyon-sandbox-${sandbox_id}-.*.constraints" 139 | } 140 | 141 | 142 | format_partial_sandbox_constraints_file_name_pattern () { 143 | local tag 144 | expect_args tag -- "$@" 145 | 146 | local sandbox_magic_hash 147 | sandbox_magic_hash=$( get_tag_sandbox_magic_hash "${tag}" ) 148 | 149 | echo "halcyon-sandbox-.*${sandbox_magic_hash:+.${sandbox_magic_hash:0:7}}-.*.constraints" 150 | } 151 | 152 | 153 | format_sandbox_common_file_name_prefix () { 154 | echo "halcyon-sandbox-" 155 | } 156 | 157 | 158 | format_sandbox_common_file_name_pattern () { 159 | local tag 160 | expect_args tag -- "$@" 161 | 162 | local label 163 | label=$( get_tag_label "${tag}" ) 164 | 165 | echo "halcyon-sandbox-.*-${label}.(tar.gz|constraints)" 166 | } 167 | 168 | 169 | format_sandbox_constraints_file_name_label () { 170 | local constraints_name 171 | expect_args constraints_name -- "$@" 172 | 173 | local label_etc 174 | label_etc="${constraints_name#halcyon-sandbox-*-}" 175 | 176 | echo "${label_etc%.constraints}" 177 | } 178 | 179 | 180 | hash_sandbox_magic () { 181 | local source_dir 182 | expect_args source_dir -- "$@" 183 | 184 | local sandbox_magic_hash 185 | if ! sandbox_magic_hash=$( hash_tree "${source_dir}/.halcyon" \( -path './ghc*' -or -path './sandbox*' \) ); then 186 | log_error 'Failed to hash sandbox magic files' 187 | return 1 188 | fi 189 | 190 | echo "${sandbox_magic_hash}" 191 | } 192 | 193 | 194 | copy_sandbox_magic () { 195 | expect_vars HALCYON_BASE 196 | 197 | local source_dir 198 | expect_args source_dir -- "$@" 199 | 200 | expect_existing "${HALCYON_BASE}/sandbox" || return 1 201 | 202 | local sandbox_magic_hash 203 | sandbox_magic_hash=$( hash_sandbox_magic "${source_dir}" ) || return 1 204 | if [[ -z "${sandbox_magic_hash}" ]]; then 205 | return 0 206 | fi 207 | 208 | local file 209 | find_tree "${source_dir}/.halcyon" -type f \( -path './ghc*' -or -path './sandbox*' \) | 210 | while read -r file; do 211 | copy_file "${source_dir}/.halcyon/${file}" \ 212 | "${HALCYON_BASE}/sandbox/.halcyon/${file}" || return 1 213 | done || return 0 214 | } 215 | 216 | 217 | add_sandbox_sources () { 218 | expect_vars HALCYON_BASE 219 | 220 | local source_dir 221 | expect_args source_dir -- "$@" 222 | 223 | if [[ ! -f "${source_dir}/.halcyon/sandbox-sources" ]]; then 224 | return 0 225 | fi 226 | 227 | local sandbox_sources sources_dir 228 | sandbox_sources=$( <"${source_dir}/.halcyon/sandbox-sources" ) || true 229 | sources_dir="${HALCYON_BASE}/sandbox/.halcyon-sandbox-sources" 230 | 231 | local name 232 | git_acquire_all "${source_dir}" "${sandbox_sources}" "${sources_dir}" | 233 | while read -r name; do 234 | log "Adding sandbox source: ${name}" 235 | 236 | sandboxed_cabal_do "${source_dir}" sandbox add-source \ 237 | "${sources_dir}/${name}" || return 1 238 | done || return 0 239 | } 240 | 241 | 242 | install_sandbox_extra_os_packages () { 243 | expect_vars HALCYON_BASE 244 | 245 | local tag source_dir 246 | expect_args tag source_dir -- "$@" 247 | 248 | if [[ ! -f "${source_dir}/.halcyon/sandbox-extra-os-packages" ]]; then 249 | return 0 250 | fi 251 | 252 | local extra_packages 253 | extra_packages=$( <"${source_dir}/.halcyon/sandbox-extra-os-packages" ) || true 254 | 255 | log 'Installing sandbox extra OS packages' 256 | 257 | if ! install_platform_packages "${extra_packages}" "${HALCYON_BASE}/sandbox"; then 258 | log_error 'Failed to install sandbox extra OS packages' 259 | return 1 260 | fi 261 | } 262 | 263 | 264 | install_sandbox_extra_apps () { 265 | expect_vars HALCYON_BASE 266 | 267 | local tag source_dir 268 | expect_args tag source_dir -- "$@" 269 | 270 | if [[ ! -f "${source_dir}/.halcyon/sandbox-extra-apps" ]]; then 271 | return 0 272 | fi 273 | 274 | local ghc_version ghc_magic_hash 275 | ghc_version=$( get_tag_ghc_version "${tag}" ) 276 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 277 | 278 | local cabal_version cabal_magic_hash cabal_remote_repo 279 | cabal_version=$( get_tag_cabal_version "${tag}" ) 280 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 281 | cabal_remote_repo=$( get_tag_cabal_remote_repo "${tag}" ) 282 | 283 | local extra_constraints 284 | extra_constraints="${source_dir}/.halcyon/sandbox-extra-apps-constraints" 285 | 286 | local -a opts_a 287 | opts_a=() 288 | opts_a+=( --root='/' ) 289 | opts_a+=( --prefix="${HALCYON_BASE}/sandbox" ) 290 | opts_a+=( --ghc-version="${ghc_version}" ) 291 | opts_a+=( --cabal-version="${cabal_version}" ) 292 | opts_a+=( --cabal-remote-repo="${cabal_remote_repo}" ) 293 | [[ -e "${extra_constraints}" ]] && opts_a+=( --constraints="${extra_constraints}" ) 294 | 295 | log 'Installing sandbox extra apps' 296 | 297 | local extra_app index 298 | index=0 299 | while read -r extra_app; do 300 | local thing 301 | if [[ -d "${source_dir}/${extra_app}" ]]; then 302 | thing="${source_dir}/${extra_app}" 303 | else 304 | thing="${extra_app}" 305 | fi 306 | 307 | index=$(( index + 1 )) 308 | if (( index > 1 )); then 309 | log 310 | fi 311 | 312 | # NOTE: Returns 2 if build is needed. 313 | HALCYON_INTERNAL_RECURSIVE=1 \ 314 | HALCYON_INTERNAL_GHC_MAGIC_HASH="${ghc_magic_hash}" \ 315 | HALCYON_INTERNAL_CABAL_MAGIC_HASH="${cabal_magic_hash}" \ 316 | HALCYON_INTERNAL_NO_COPY_LOCAL_SOURCE=1 \ 317 | halcyon install "${opts_a[@]}" "${thing}" 2>&1 | quote || return 318 | done <"${source_dir}/.halcyon/sandbox-extra-apps" || return 0 319 | } 320 | 321 | 322 | build_sandbox_dir () { 323 | expect_vars HALCYON_BASE \ 324 | HALCYON_SANDBOX_NO_REMOVE_DOC HALCYON_SANDBOX_NO_STRIP 325 | 326 | local tag source_dir constraints must_create 327 | expect_args tag source_dir constraints must_create -- "$@" 328 | 329 | local ghc_version cabal_version 330 | ghc_version=$( get_tag_ghc_version "${tag}" ) 331 | cabal_version=$( get_tag_cabal_version "${tag}" ) 332 | 333 | if (( must_create )); then 334 | if ! rm -rf "${HALCYON_BASE}/sandbox"; then 335 | log_error 'Failed to remove sandbox directory' 336 | return 1 337 | fi 338 | else 339 | expect_existing "${HALCYON_BASE}/sandbox/.halcyon-tag" \ 340 | "${HALCYON_BASE}/sandbox/.halcyon-constraints" || return 1 341 | fi 342 | expect_existing "${source_dir}" || return 1 343 | 344 | log 'Building sandbox directory' 345 | 346 | if (( must_create )); then 347 | log 'Creating sandbox' 348 | 349 | if ! cabal_create_sandbox; then 350 | log_error 'Failed to create sandbox' 351 | return 1 352 | fi 353 | fi 354 | 355 | if ! add_sandbox_sources "${source_dir}"; then 356 | log_error 'Failed to add sandbox sources' 357 | return 1 358 | fi 359 | 360 | # NOTE: Returns 2 if build is needed. 361 | install_sandbox_extra_apps "${tag}" "${source_dir}" || return 362 | 363 | install_sandbox_extra_os_packages "${tag}" "${source_dir}" || return 1 364 | 365 | if [[ -f "${source_dir}/.halcyon/sandbox-pre-build-hook" ]]; then 366 | log 'Executing sandbox pre-build hook' 367 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 368 | HALCYON_GHC_VERSION="${ghc_version}" \ 369 | HALCYON_CABAL_VERSION="${cabal_version}" \ 370 | "${source_dir}/.halcyon/sandbox-pre-build-hook" \ 371 | "${tag}" "${source_dir}" "${constraints}" 2>&1 | quote 372 | then 373 | log_error 'Failed to execute sandbox pre-build hook' 374 | return 1 375 | fi 376 | log 'Sandbox pre-build hook executed' 377 | fi 378 | 379 | local -a opts_a 380 | opts_a=() 381 | opts_a+=( --dependencies-only ) 382 | [[ -e "${HALCYON_BASE}/sandbox/include" ]] && opts_a+=( --extra-include-dirs="${HALCYON_BASE}/sandbox/include" ) 383 | [[ -e "${HALCYON_BASE}/sandbox/usr/include" ]] && opts_a+=( --extra-include-dirs="${HALCYON_BASE}/sandbox/usr/include" ) 384 | [[ -e "${HALCYON_BASE}/sandbox/lib" ]] && opts_a+=( --extra-lib-dirs="${HALCYON_BASE}/sandbox/lib" ) 385 | [[ -e "${HALCYON_BASE}/sandbox/usr/lib" ]] && opts_a+=( --extra-lib-dirs="${HALCYON_BASE}/sandbox/usr/lib" ) 386 | 387 | case "${HALCYON_INTERNAL_PLATFORM}" in 388 | 'linux-amzn-'*|'linux-centos-'*|'linux-fedora-'*|'linux-rhel-'*) 389 | [[ -e "${HALCYON_BASE}/sandbox/lib64" ]] && opts_a+=( --extra-lib-dirs="${HALCYON_BASE}/sandbox/lib64" ) 390 | [[ -e "${HALCYON_BASE}/sandbox/usr/lib64" ]] && opts_a+=( --extra-lib-dirs="${HALCYON_BASE}/sandbox/usr/lib64" ) 391 | ;; 392 | 'linux-debian-'*'-i386'|'linux-ubuntu-'*'-i386') 393 | [[ -e "${HALCYON_BASE}/sandbox/include/i386-linux-gnu" ]] && opts_a+=( --extra-include-dirs="${HALCYON_BASE}/sandbox/include/i386-linux-gnu" ) 394 | [[ -e "${HALCYON_BASE}/sandbox/usr/include/i386-linux-gnu" ]] && opts_a+=( --extra-include-dirs="${HALCYON_BASE}/sandbox/usr/include/i386-linux-gnu" ) 395 | [[ -e "${HALCYON_BASE}/sandbox/lib/i386-linux-gnu" ]] && opts_a+=( --extra-lib-dirs="${HALCYON_BASE}/sandbox/lib/i386-linux-gnu" ) 396 | [[ -e "${HALCYON_BASE}/sandbox/usr/lib/i386-linux-gnu" ]] && opts_a+=( --extra-lib-dirs="${HALCYON_BASE}/sandbox/usr/lib/i386-linux-gnu" ) 397 | ;; 398 | 'linux-debian-'*'-x86_64'|'linux-ubuntu-'*'-x86_64') 399 | [[ -e "${HALCYON_BASE}/sandbox/include/x86_64-linux-gnu" ]] && opts_a+=( --extra-include-dirs="${HALCYON_BASE}/sandbox/include/x86_64-linux-gnu" ) 400 | [[ -e "${HALCYON_BASE}/sandbox/usr/include/x86_64-linux-gnu" ]] && opts_a+=( --extra-include-dirs="${HALCYON_BASE}/sandbox/usr/include/x86_64-linux-gnu" ) 401 | [[ -e "${HALCYON_BASE}/sandbox/lib/x86_64-linux-gnu" ]] && opts_a+=( --extra-lib-dirs="${HALCYON_BASE}/sandbox/lib/x86_64-linux-gnu" ) 402 | [[ -e "${HALCYON_BASE}/sandbox/usr/lib/x86_64-linux-gnu" ]] && opts_a+=( --extra-lib-dirs="${HALCYON_BASE}/sandbox/usr/lib/x86_64-linux-gnu" ) 403 | esac 404 | 405 | if [[ -f "${source_dir}/.halcyon/sandbox-extra-configure-flags" ]]; then 406 | opts_a=( $( <"${source_dir}/.halcyon/sandbox-extra-configure-flags" ) ) || true 407 | fi 408 | 409 | log 'Building sandbox' 410 | 411 | local built_size 412 | if ! sandboxed_cabal_do "${source_dir}" install "${opts_a[@]}" 2>&1 | quote || 413 | ! copy_sandbox_magic "${source_dir}" || 414 | ! built_size=$( get_size "${HALCYON_BASE}/sandbox" ) 415 | then 416 | log_error 'Failed to build sandbox' 417 | return 1 418 | fi 419 | log "Sandbox built, ${built_size}" 420 | 421 | if ! format_constraints <<<"${constraints}" \ 422 | >"${HALCYON_BASE}/sandbox/.halcyon-constraints" 423 | then 424 | log_error 'Failed to prepare sandbox directory' 425 | return 1 426 | fi 427 | 428 | if [[ -f "${source_dir}/.halcyon/sandbox-post-build-hook" ]]; then 429 | log 'Executing sandbox post-build hook' 430 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 431 | HALCYON_GHC_VERSION="${ghc_version}" \ 432 | HALCYON_CABAL_VERSION="${cabal_version}" \ 433 | "${source_dir}/.halcyon/sandbox-post-build-hook" \ 434 | "${tag}" "${source_dir}" "${constraints}" 2>&1 | quote 435 | then 436 | log_error 'Failed to execute sandbox post-build hook' 437 | return 1 438 | fi 439 | log 'Sandbox post-build hook executed' 440 | fi 441 | 442 | if ! (( HALCYON_SANDBOX_NO_REMOVE_DOC )) && 443 | [[ -d "${HALCYON_BASE}/sandbox/logs" || -d "${HALCYON_BASE}/sandbox/share/doc" ]] 444 | then 445 | log_indent_begin 'Removing documentation from sandbox directory...' 446 | 447 | local trimmed_size 448 | if ! rm -rf "${HALCYON_BASE}/sandbox/logs" "${HALCYON_BASE}/sandbox/share/doc" || 449 | ! trimmed_size=$( get_size "${HALCYON_BASE}/sandbox" ) 450 | then 451 | log_indent_end 'error' 452 | return 1 453 | fi 454 | log_indent_end "done, ${trimmed_size}" 455 | fi 456 | 457 | if ! (( HALCYON_SANDBOX_NO_STRIP )); then 458 | log_indent_begin 'Stripping sandbox directory...' 459 | 460 | local stripped_size 461 | if ! strip_tree "${HALCYON_BASE}/sandbox" || 462 | ! stripped_size=$( get_size "${HALCYON_BASE}/sandbox" ) 463 | then 464 | log_indent_end 'error' 465 | return 1 466 | fi 467 | log_indent_end "done, ${stripped_size}" 468 | fi 469 | 470 | if ! derive_sandbox_tag "${tag}" >"${HALCYON_BASE}/sandbox/.halcyon-tag"; then 471 | log_error 'Failed to write sandbox tag' 472 | return 1 473 | fi 474 | } 475 | 476 | 477 | archive_sandbox_dir () { 478 | expect_vars HALCYON_BASE HALCYON_CACHE HALCYON_NO_ARCHIVE \ 479 | HALCYON_INTERNAL_PLATFORM 480 | 481 | if (( HALCYON_NO_ARCHIVE )); then 482 | return 0 483 | fi 484 | 485 | expect_existing "${HALCYON_BASE}/sandbox/.halcyon-tag" \ 486 | "${HALCYON_BASE}/sandbox/.halcyon-constraints" || return 1 487 | 488 | local sandbox_tag ghc_id archive_name constraints_name 489 | sandbox_tag=$( detect_sandbox_tag "${HALCYON_BASE}/sandbox/.halcyon-tag" ) || return 1 490 | ghc_id=$( format_ghc_id "${sandbox_tag}" ) 491 | archive_name=$( format_sandbox_archive_name "${sandbox_tag}" ) 492 | constraints_name=$( format_sandbox_constraints_file_name "${sandbox_tag}" ) 493 | 494 | log 'Archiving sandbox directory' 495 | 496 | create_cached_archive "${HALCYON_BASE}/sandbox" "${archive_name}" || return 1 497 | 498 | if ! copy_file "${HALCYON_BASE}/sandbox/.halcyon-constraints" \ 499 | "${HALCYON_CACHE}/${constraints_name}" 500 | then 501 | log_error 'Failed to cache file' 502 | return 1 503 | fi 504 | 505 | upload_cached_file "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${archive_name}" || return 1 506 | upload_cached_file "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${constraints_name}" || return 1 507 | 508 | local common_prefix common_pattern 509 | common_prefix=$( format_sandbox_common_file_name_prefix ) 510 | common_pattern=$( format_sandbox_common_file_name_pattern "${sandbox_tag}" ) 511 | 512 | delete_matching_private_stored_files "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${common_prefix}" "${common_pattern}" "(${archive_name}|${constraints_name})" || return 1 513 | } 514 | 515 | 516 | validate_sandbox_dir () { 517 | expect_vars HALCYON_BASE 518 | 519 | local tag 520 | expect_args tag -- "$@" 521 | 522 | local sandbox_tag candidate_tag 523 | sandbox_tag=$( derive_sandbox_tag "${tag}" ) 524 | candidate_tag=$( detect_tag "${HALCYON_BASE}/sandbox/.halcyon-tag" "${sandbox_tag//./\.}" ) || return 1 525 | 526 | if [[ ! -f "${HALCYON_BASE}/sandbox/cabal.sandbox.config" ]]; then 527 | return 1 528 | fi 529 | 530 | echo "${candidate_tag}" 531 | } 532 | 533 | 534 | restore_sandbox_dir () { 535 | expect_vars HALCYON_BASE \ 536 | HALCYON_INTERNAL_PLATFORM 537 | 538 | local tag 539 | expect_args tag -- "$@" 540 | 541 | local ghc_id archive_name 542 | ghc_id=$( format_ghc_id "${tag}" ) 543 | archive_name=$( format_sandbox_archive_name "${tag}" ) 544 | 545 | if validate_sandbox_dir "${tag}" >'/dev/null'; then 546 | log 'Using existing sandbox directory' 547 | 548 | touch_cached_file "${archive_name}" 549 | return 0 550 | fi 551 | rm -rf "${HALCYON_BASE}/sandbox" || true 552 | 553 | log 'Restoring sandbox directory' 554 | 555 | if ! extract_cached_archive_over "${archive_name}" "${HALCYON_BASE}/sandbox" || 556 | ! validate_sandbox_dir "${tag}" >'/dev/null' 557 | then 558 | rm -rf "${HALCYON_BASE}/sandbox" || true 559 | cache_stored_file "${HALCYON_INTERNAL_PLATFORM}/ghc-${ghc_id}" "${archive_name}" || return 1 560 | 561 | if ! extract_cached_archive_over "${archive_name}" "${HALCYON_BASE}/sandbox" || 562 | ! validate_sandbox_dir "${tag}" >'/dev/null' 563 | then 564 | rm -rf "${HALCYON_BASE}/sandbox" || true 565 | 566 | log_warning 'Failed to restore sandbox directory' 567 | return 1 568 | fi 569 | else 570 | touch_cached_file "${archive_name}" 571 | fi 572 | } 573 | 574 | 575 | get_sandbox_package_db () { 576 | expect_vars HALCYON_BASE 577 | 578 | filter_matching '^package-db: ' <"${HALCYON_BASE}/sandbox/cabal.sandbox.config" | 579 | match_exactly_one | 580 | awk '{ print $2 }' || return 1 581 | } 582 | 583 | 584 | recache_sandbox_package_db () { 585 | local package_db 586 | if ! package_db=$( get_sandbox_package_db ) || 587 | ! ghc-pkg recache --package-db="${package_db}" 2>&1 | quote 588 | then 589 | log_warning 'Failed to recache sandbox package database' 590 | fi 591 | } 592 | 593 | 594 | install_matching_sandbox_dir () { 595 | expect_vars HALCYON_BASE 596 | 597 | local tag source_dir constraints matching_tag 598 | expect_args tag source_dir constraints matching_tag -- "$@" 599 | 600 | local constraints_hash matching_hash matching_description 601 | constraints_hash=$( get_tag_constraints_hash "${tag}" ) 602 | matching_hash=$( get_tag_constraints_hash "${matching_tag}" ) 603 | matching_description=$( format_sandbox_description "${matching_tag}" ) 604 | 605 | if [[ "${matching_hash}" == "${constraints_hash}" ]]; then 606 | log "Using fully-matching sandbox directory: ${matching_description}" 607 | 608 | HALCYON_NO_UPLOAD=1 \ 609 | restore_sandbox_dir "${matching_tag}" || return 1 610 | recache_sandbox_package_db 611 | 612 | if ! derive_sandbox_tag "${tag}" >"${HALCYON_BASE}/sandbox/.halcyon-tag"; then 613 | log_error 'Failed to write sandbox tag' 614 | return 1 615 | fi 616 | return 0 617 | fi 618 | 619 | log "Using partially-matching sandbox directory: ${matching_description}" 620 | 621 | HALCYON_NO_UPLOAD=1 \ 622 | restore_sandbox_dir "${matching_tag}" || return 1 623 | recache_sandbox_package_db 624 | 625 | local must_create 626 | must_create=0 627 | build_sandbox_dir "${tag}" "${source_dir}" "${constraints}" "${must_create}" || return 1 628 | } 629 | 630 | 631 | install_sandbox_dir () { 632 | expect_vars HALCYON_NO_BUILD HALCYON_NO_BUILD_DEPENDENCIES \ 633 | HALCYON_SANDBOX_REBUILD 634 | 635 | local tag source_dir constraints 636 | expect_args tag source_dir constraints -- "$@" 637 | 638 | if ! (( HALCYON_SANDBOX_REBUILD )); then 639 | if restore_sandbox_dir "${tag}"; then 640 | recache_sandbox_package_db 641 | return 0 642 | fi 643 | 644 | # NOTE: If Halcyon fails to build the sandbox on top of a 645 | # matching sandbox, it will attempt to build the sandbox 646 | # from scratch. 647 | local matching_tag 648 | if matching_tag=$( match_sandbox_dir "${tag}" "${constraints}" ) && 649 | install_matching_sandbox_dir "${tag}" "${source_dir}" "${constraints}" "${matching_tag}" 650 | then 651 | archive_sandbox_dir || return 1 652 | return 0 653 | fi 654 | 655 | # NOTE: Returns 2 if build is needed. 656 | if (( HALCYON_NO_BUILD )) || (( HALCYON_NO_BUILD_DEPENDENCIES )); then 657 | log_error 'Cannot build sandbox directory' 658 | return 2 659 | fi 660 | fi 661 | 662 | # NOTE: Returns 2 if build is needed. 663 | local must_create 664 | must_create=1 665 | build_sandbox_dir "${tag}" "${source_dir}" "${constraints}" "${must_create}" || return 666 | archive_sandbox_dir || return 1 667 | } 668 | -------------------------------------------------------------------------------- /src/main.sh: -------------------------------------------------------------------------------- 1 | set_halcyon_vars () { 2 | if ! (( ${HALCYON_INTERNAL_VARS:-0} )); then 3 | export HALCYON_INTERNAL_VARS=1 4 | 5 | export HALCYON_DEFAULT_ROOT='/' 6 | export HALCYON_DEFAULT_CACHE='/var/tmp/halcyon-cache' 7 | export HALCYON_DEFAULT_PUBLIC_STORAGE_URL='https://halcyon.global.ssl.fastly.net' 8 | export HALCYON_DEFAULT_PUBLIC_STORAGE_S3_BUCKET='s3.halcyon.sh' 9 | export HALCYON_DEFAULT_S3_ENDPOINT='s3.amazonaws.com' 10 | export HALCYON_DEFAULT_S3_ACL='private' 11 | export HALCYON_DEFAULT_GHC_VERSION='7.8.4' 12 | export HALCYON_DEFAULT_CABAL_VERSION='1.20.0.3' 13 | export HALCYON_DEFAULT_CABAL_REMOTE_REPO='hackage:http://hackage.haskell.org/packages/archive' 14 | 15 | # NOTE: HALCYON_BASE is set in paths.sh. 16 | export HALCYON_PREFIX="${HALCYON_PREFIX:-${HALCYON_BASE}}" 17 | export HALCYON_ROOT="${HALCYON_ROOT:-${HALCYON_DEFAULT_ROOT}}" 18 | export HALCYON_NO_APP="${HALCYON_NO_APP:-0}" 19 | export HALCYON_LOG_TIMESTAMP="${HALCYON_LOG_TIMESTAMP:-0}" 20 | 21 | export HALCYON_EXTRA_SOURCE_HASH_IGNORE="${HALCYON_EXTRA_SOURCE_HASH_IGNORE:-}" 22 | export HALCYON_CONSTRAINTS="${HALCYON_CONSTRAINTS:-}" 23 | export HALCYON_IGNORE_ALL_CONSTRAINTS="${HALCYON_IGNORE_ALL_CONSTRAINTS:-0}" 24 | export HALCYON_EXTRA_CONFIGURE_FLAGS="${HALCYON_EXTRA_CONFIGURE_FLAGS:-}" 25 | export HALCYON_PRE_BUILD_HOOK="${HALCYON_PRE_BUILD_HOOK:-}" 26 | export HALCYON_POST_BUILD_HOOK="${HALCYON_POST_BUILD_HOOK:-}" 27 | export HALCYON_APP_REBUILD="${HALCYON_APP_REBUILD:-0}" 28 | export HALCYON_APP_RECONFIGURE="${HALCYON_APP_RECONFIGURE:-0}" 29 | export HALCYON_APP_NO_REMOVE_DOC="${HALCYON_APP_NO_REMOVE_DOC:-0}" 30 | export HALCYON_APP_NO_STRIP="${HALCYON_APP_NO_STRIP:-0}" 31 | export HALCYON_NO_BUILD="${HALCYON_NO_BUILD:-0}" 32 | export HALCYON_NO_BUILD_DEPENDENCIES="${HALCYON_NO_BUILD_DEPENDENCIES:-0}" 33 | export HALCYON_DEPENDENCIES_ONLY="${HALCYON_DEPENDENCIES_ONLY:-0}" 34 | 35 | export HALCYON_EXTRA_APPS="${HALCYON_EXTRA_APPS:-}" 36 | export HALCYON_EXTRA_APPS_CONSTRAINTS="${HALCYON_EXTRA_APPS_CONSTRAINTS:-}" 37 | export HALCYON_EXTRA_DATA_FILES="${HALCYON_EXTRA_DATA_FILES:-}" 38 | export HALCYON_EXTRA_OS_PACKAGES="${HALCYON_EXTRA_OS_PACKAGES:-}" 39 | export HALCYON_PRE_INSTALL_HOOK="${HALCYON_PRE_INSTALL_HOOK:-}" 40 | export HALCYON_POST_INSTALL_HOOK="${HALCYON_POST_INSTALL_HOOK:-}" 41 | export HALCYON_APP_REINSTALL="${HALCYON_APP_REINSTALL:-0}" 42 | export HALCYON_KEEP_DEPENDENCIES="${HALCYON_KEEP_DEPENDENCIES:-0}" 43 | 44 | export HALCYON_CACHE="${HALCYON_CACHE:-${HALCYON_DEFAULT_CACHE}}" 45 | export HALCYON_PURGE_CACHE="${HALCYON_PURGE_CACHE:-0}" 46 | export HALCYON_NO_ARCHIVE="${HALCYON_NO_ARCHIVE:-0}" 47 | export HALCYON_NO_CLEAN_CACHE="${HALCYON_NO_CLEAN_CACHE:-0}" 48 | 49 | export HALCYON_PUBLIC_STORAGE_URL="${HALCYON_PUBLIC_STORAGE_URL:-${HALCYON_DEFAULT_PUBLIC_STORAGE_URL}}" 50 | export HALCYON_NO_PUBLIC_STORAGE="${HALCYON_NO_PUBLIC_STORAGE:-0}" 51 | 52 | export HALCYON_AWS_ACCESS_KEY_ID="${HALCYON_AWS_ACCESS_KEY_ID:-}" 53 | export HALCYON_AWS_SECRET_ACCESS_KEY="${HALCYON_AWS_SECRET_ACCESS_KEY:-}" 54 | export HALCYON_S3_BUCKET="${HALCYON_S3_BUCKET:-}" 55 | export HALCYON_S3_ENDPOINT="${HALCYON_S3_ENDPOINT:-${HALCYON_DEFAULT_S3_ENDPOINT}}" 56 | export HALCYON_S3_ACL="${HALCYON_S3_ACL:-${HALCYON_DEFAULT_S3_ACL}}" 57 | export HALCYON_NO_PRIVATE_STORAGE="${HALCYON_NO_PRIVATE_STORAGE:-0}" 58 | export HALCYON_NO_UPLOAD="${HALCYON_NO_UPLOAD:-0}" 59 | export HALCYON_NO_CLEAN_PRIVATE_STORAGE="${HALCYON_NO_CLEAN_PRIVATE_STORAGE:-0}" 60 | 61 | export HALCYON_GHC_VERSION="${HALCYON_GHC_VERSION:-${HALCYON_DEFAULT_GHC_VERSION}}" 62 | export HALCYON_GHC_PRE_BUILD_HOOK="${HALCYON_GHC_PRE_BUILD_HOOK:-}" 63 | export HALCYON_GHC_POST_BUILD_HOOK="${HALCYON_GHC_POST_BUILD_HOOK:-}" 64 | export HALCYON_GHC_REBUILD="${HALCYON_GHC_REBUILD:-0}" 65 | export HALCYON_GHC_NO_REMOVE_DOC="${HALCYON_GHC_NO_REMOVE_DOC:-0}" 66 | export HALCYON_GHC_NO_STRIP="${HALCYON_GHC_NO_STRIP:-0}" 67 | 68 | export HALCYON_CABAL_VERSION="${HALCYON_CABAL_VERSION:-}" 69 | export HALCYON_CABAL_REMOTE_REPO="${HALCYON_CABAL_REMOTE_REPO:-}" 70 | export HALCYON_CABAL_PRE_BUILD_HOOK="${HALCYON_CABAL_PRE_BUILD_HOOK:-}" 71 | export HALCYON_CABAL_POST_BUILD_HOOK="${HALCYON_CABAL_POST_BUILD_HOOK:-}" 72 | export HALCYON_CABAL_PRE_UPDATE_HOOK="${HALCYON_CABAL_PRE_UPDATE_HOOK:-}" 73 | export HALCYON_CABAL_POST_UPDATE_HOOK="${HALCYON_CABAL_POST_UPDATE_HOOK:-}" 74 | export HALCYON_CABAL_REBUILD="${HALCYON_CABAL_REBUILD:-0}" 75 | export HALCYON_CABAL_UPDATE="${HALCYON_CABAL_UPDATE:-0}" 76 | export HALCYON_CABAL_NO_STRIP="${HALCYON_CABAL_NO_STRIP:-0}" 77 | export HALCYON_CABAL_NO_UPDATE="${HALCYON_CABAL_NO_UPDATE:-0}" 78 | 79 | export HALCYON_SANDBOX_EXTRA_CONFIGURE_FLAGS="${HALCYON_SANDBOX_EXTRA_CONFIGURE_FLAGS:-}" 80 | export HALCYON_SANDBOX_SOURCES="${HALCYON_SANDBOX_SOURCES:-}" 81 | export HALCYON_SANDBOX_EXTRA_APPS="${HALCYON_SANDBOX_EXTRA_APPS:-}" 82 | export HALCYON_SANDBOX_EXTRA_APPS_CONSTRAINTS="${HALCYON_SANDBOX_EXTRA_APPS_CONSTRAINTS:-}" 83 | export HALCYON_SANDBOX_EXTRA_OS_PACKAGES="${HALCYON_SANDBOX_EXTRA_OS_PACKAGES:-}" 84 | export HALCYON_SANDBOX_PRE_BUILD_HOOK="${HALCYON_SANDBOX_PRE_BUILD_HOOK:-}" 85 | export HALCYON_SANDBOX_POST_BUILD_HOOK="${HALCYON_SANDBOX_POST_BUILD_HOOK:-}" 86 | export HALCYON_SANDBOX_REBUILD="${HALCYON_SANDBOX_REBUILD:-0}" 87 | export HALCYON_SANDBOX_NO_REMOVE_DOC="${HALCYON_SANDBOX_NO_REMOVE_DOC:-0}" 88 | export HALCYON_SANDBOX_NO_STRIP="${HALCYON_SANDBOX_NO_STRIP:-0}" 89 | 90 | export HALCYON_INTERNAL_COMMAND="${HALCYON_INTERNAL_COMMAND:-}" 91 | export HALCYON_INTERNAL_RECURSIVE="${HALCYON_INTERNAL_RECURSIVE:-0}" 92 | export HALCYON_INTERNAL_REMOTE_SOURCE="${HALCYON_INTERNAL_REMOTE_SOURCE:-0}" 93 | export HALCYON_INTERNAL_TOLERATE_GHC_USER_DB="${HALCYON_INTERNAL_TOLERATE_GHC_USER_DB:-0}" 94 | export HALCYON_INTERNAL_NO_ANNOUNCE_INSTALL="${HALCYON_INTERNAL_NO_ANNOUNCE_INSTALL:-0}" 95 | export HALCYON_INTERNAL_NO_CLEANUP="${HALCYON_INTERNAL_NO_CLEANUP:-0}" 96 | export HALCYON_INTERNAL_NO_COPY_LOCAL_SOURCE="${HALCYON_INTERNAL_NO_COPY_LOCAL_SOURCE:-0}" 97 | export HALCYON_INTERNAL_CABAL_MAGIC_HASH="${HALCYON_INTERNAL_CABAL_MAGIC_HASH:-}" 98 | export HALCYON_INTERNAL_GHC_MAGIC_HASH="${HALCYON_INTERNAL_GHC_MAGIC_HASH:-}" 99 | fi 100 | 101 | if (( HALCYON_INTERNAL_RECURSIVE )); then 102 | export HALCYON_LOG_TIMESTAMP=0 103 | 104 | export HALCYON_EXTRA_SOURCE_HASH_IGNORE='' 105 | export HALCYON_CONSTRAINTS='' 106 | export HALCYON_IGNORE_ALL_CONSTRAINTS=0 107 | export HALCYON_EXTRA_CONFIGURE_FLAGS='' 108 | export HALCYON_PRE_BUILD_HOOK='' 109 | export HALCYON_POST_BUILD_HOOK='' 110 | export HALCYON_APP_REBUILD=0 111 | export HALCYON_APP_RECONFIGURE=0 112 | export HALCYON_DEPENDENCIES_ONLY=0 113 | 114 | export HALCYON_EXTRA_APPS='' 115 | export HALCYON_EXTRA_APPS_CONSTRAINTS='' 116 | export HALCYON_EXTRA_DATA_FILES='' 117 | export HALCYON_EXTRA_OS_PACKAGES='' 118 | export HALCYON_PRE_INSTALL_HOOK='' 119 | export HALCYON_POST_INSTALL_HOOK='' 120 | export HALCYON_APP_REINSTALL=0 121 | export HALCYON_KEEP_DEPENDENCIES=0 122 | 123 | export HALCYON_GHC_REBUILD=0 124 | 125 | export HALCYON_CABAL_REBUILD=0 126 | export HALCYON_CABAL_UPDATE=0 127 | 128 | export HALCYON_SANDBOX_EXTRA_CONFIGURE_FLAGS='' 129 | export HALCYON_SANDBOX_SOURCES='' 130 | export HALCYON_SANDBOX_EXTRA_APPS='' 131 | export HALCYON_SANDBOX_EXTRA_APPS_CONSTRAINTS='' 132 | export HALCYON_SANDBOX_EXTRA_OS_PACKAGES='' 133 | export HALCYON_SANDBOX_PRE_BUILD_HOOK='' 134 | export HALCYON_SANDBOX_POST_BUILD_HOOK='' 135 | export HALCYON_SANDBOX_REBUILD=0 136 | 137 | export HALCYON_INTERNAL_NO_ANNOUNCE_INSTALL=0 138 | fi 139 | } 140 | 141 | 142 | halcyon_main () { 143 | expect_vars HALCYON_DIR 144 | 145 | expect_existing "${HALCYON_DIR}" || return 1 146 | 147 | local cmd 148 | local -a args_a 149 | cmd='' 150 | args_a=() 151 | 152 | while (( $# )); do 153 | case "$1" in 154 | 155 | # General options 156 | '--base') 157 | shift 158 | local base_dir 159 | expect_args base_dir -- "$@" 160 | export HALCYON_BASE="${base_dir}";; 161 | '--base='*) 162 | export HALCYON_BASE="${1#*=}";; 163 | '--prefix') 164 | shift 165 | local prefix 166 | expect_args prefix -- "$@" 167 | export HALCYON_PREFIX="${prefix}";; 168 | '--prefix='*) 169 | export HALCYON_PREFIX="${1#*=}";; 170 | '--root') 171 | shift 172 | local root 173 | expect_args root -- "$@" 174 | export HALCYON_ROOT="${root}";; 175 | '--root='*) 176 | export HALCYON_ROOT="${1#*=}";; 177 | '--no-app') 178 | export HALCYON_NO_APP=1;; 179 | '--log-timestamp') 180 | export HALCYON_LOG_TIMESTAMP=1;; 181 | 182 | # Build-time options 183 | '--extra-source-hash-ignore') 184 | shift 185 | local extra_source_hash_ignore 186 | expect_args extra_source_hash_ignore -- "$@" 187 | export HALCYON_EXTRA_SOURCE_HASH_IGNORE="${extra_source_hash_ignore}";; 188 | '--extra-source-hash-ignore='*) 189 | export HALCYON_EXTRA_SOURCE_HASH_IGNORE="${1#*=}";; 190 | '--constraints') 191 | shift 192 | local constraints 193 | expect_args constraints -- "$@" 194 | export HALCYON_CONSTRAINTS="${constraints}";; 195 | '--constraints='*) 196 | export HALCYON_CONSTRAINTS="${1#*=}";; 197 | '--extra-configure-flags') 198 | shift 199 | local extra_configure_flags 200 | expect_args extra_configure_flags -- "$@" 201 | export HALCYON_EXTRA_CONFIGURE_FLAGS="${extra_configure_flags}";; 202 | '--extra-configure-flags='*) 203 | export HALCYON_EXTRA_CONFIGURE_FLAGS="${1#*=}";; 204 | '--pre-build-hook') 205 | shift 206 | local pre_build_hook 207 | expect_args pre_build_hook -- "$@" 208 | export HALCYON_PRE_BUILD_HOOK="${pre_build_hook}";; 209 | '--pre-build-hook='*) 210 | export HALCYON_PRE_BUILD_HOOK="${1#*=}";; 211 | '--post-build-hook') 212 | shift 213 | local post_build_hook 214 | expect_args post_build_hook -- "$@" 215 | export HALCYON_POST_BUILD_HOOK="${post_build_hook}";; 216 | '--post-build-hook='*) 217 | export HALCYON_POST_BUILD_HOOK="${1#*=}";; 218 | '--app-rebuild') 219 | export HALCYON_APP_REBUILD=1;; 220 | '--app-reconfigure') 221 | export HALCYON_APP_RECONFIGURE=1;; 222 | '--app-no-remove-doc') 223 | export HALCYON_APP_NO_REMOVE_DOC=1;; 224 | '--app-no-strip') 225 | export HALCYON_APP_NO_STRIP=1;; 226 | '--ignore-all-constraints') 227 | export HALCYON_IGNORE_ALL_CONSTRAINTS=1;; 228 | '--no-build') 229 | export HALCYON_NO_BUILD=1;; 230 | '--no-build-dependencies') 231 | export HALCYON_NO_BUILD_DEPENDENCIES=1;; 232 | '--dependencies-only') 233 | export HALCYON_DEPENDENCIES_ONLY=1;; 234 | 235 | # Install-time options 236 | '--extra-apps') 237 | shift 238 | local extra_apps 239 | expect_args extra_apps -- "$@" 240 | export HALCYON_EXTRA_APPS="${extra_apps}";; 241 | '--extra-apps='*) 242 | export HALCYON_EXTRA_APPS="${1#*=}";; 243 | '--extra-apps-constraints') 244 | shift 245 | local extra_apps_constraints 246 | expect_args extra_apps_constraints -- "$@" 247 | export HALCYON_EXTRA_APPS_CONSTRAINTS="${extra_apps_constraints}";; 248 | '--extra-apps-constraints='*) 249 | export HALCYON_EXTRA_APPS_CONSTRAINTS="${1#*=}";; 250 | '--extra-data-files') 251 | shift 252 | local extra_data_files 253 | expect_args extra_data_files -- "$@" 254 | export HALCYON_EXTRA_DATA_FILES="${extra_data_files}";; 255 | '--extra-data-files='*) 256 | export HALCYON_EXTRA_DATA_FILES="${1#*=}";; 257 | '--extra-os-packages') 258 | shift 259 | local extra_os_packages 260 | expect_args extra_os_packages -- "$@" 261 | export HALCYON_EXTRA_OS_PACKAGES="${extra_os_packages}";; 262 | '--extra-os-packages='*) 263 | export HALCYON_EXTRA_OS_PACKAGES="${1#*=}";; 264 | '--pre-install-hook') 265 | shift 266 | local pre_install_hook 267 | expect_args pre_install_hook -- "$@" 268 | export HALCYON_PRE_INSTALL_HOOK="${pre_install_hook}";; 269 | '--pre-install-hook='*) 270 | export HALCYON_PRE_INSTALL_HOOK="${1#*=}";; 271 | '--post-install-hook') 272 | shift 273 | local post_install_hook 274 | expect_args post_install_hook -- "$@" 275 | export HALCYON_POST_INSTALL_HOOK="${post_install_hook}";; 276 | '--post-install-hook='*) 277 | export HALCYON_POST_INSTALL_HOOK="${1#*=}";; 278 | '--app-reinstall') 279 | export HALCYON_APP_REINSTALL=1;; 280 | '--keep-dependencies') 281 | export HALCYON_KEEP_DEPENDENCIES=1;; 282 | 283 | # Cache options 284 | '--cache') 285 | shift 286 | local cache_dir 287 | expect_args cache_dir -- "$@" 288 | export HALCYON_CACHE="${cache_dir}";; 289 | '--cache='*) 290 | export HALCYON_CACHE="${1#*=}";; 291 | '--purge-cache') 292 | export HALCYON_PURGE_CACHE=1;; 293 | '--no-archive') 294 | export HALCYON_NO_ARCHIVE=1;; 295 | '--no-clean-cache') 296 | export HALCYON_NO_CLEAN_CACHE=1;; 297 | 298 | # Public storage options 299 | '--public-storage-url') 300 | shift 301 | local public_storage_url 302 | expect_args public_storage_url -- "$@" 303 | export HALCYON_PUBLIC_STORAGE_URL="${public_storage_url}";; 304 | '--public-storage-url='*) 305 | export HALCYON_PUBLIC_STORAGE_URL="${1#*=}";; 306 | '--no-public-storage') 307 | export HALCYON_NO_PUBLIC_STORAGE=1;; 308 | 309 | # Private storage options 310 | '--aws-access-key-id') 311 | shift 312 | local aws_access_key_id 313 | expect_args aws_access_key_id -- "$@" 314 | export HALCYON_AWS_ACCESS_KEY_ID="${aws_access_key_id}";; 315 | '--aws-access-key-id='*) 316 | export HALCYON_AWS_ACCESS_KEY_ID="${1#*=}";; 317 | '--aws-secret-access-key') 318 | shift 319 | local aws_secret_access_key 320 | expect_args aws_secret_access_key -- "$@" 321 | export HALCYON_AWS_SECRET_ACCESS_KEY="${aws_secret_access_key}";; 322 | '--aws-secret-access-key='*) 323 | export HALCYON_AWS_SECRET_ACCESS_KEY="${1#*=}";; 324 | '--s3-bucket') 325 | shift 326 | local s3_bucket 327 | expect_args s3_bucket -- "$@" 328 | export HALCYON_S3_BUCKET="${s3_bucket}";; 329 | '--s3-bucket='*) 330 | export HALCYON_S3_BUCKET="${1#*=}";; 331 | '--s3-endpoint') 332 | shift 333 | local s3_endpoint 334 | expect_args s3_endpoint -- "$@" 335 | export HALCYON_S3_ENDPOINT="${s3_endpoint}";; 336 | '--s3-endpoint='*) 337 | export HALCYON_S3_ENDPOINT="${1#*=}";; 338 | '--s3-acl') 339 | shift 340 | local s3_acl 341 | expect_args s3_acl -- "$@" 342 | export HALCYON_S3_ACL="${s3_acl}";; 343 | '--s3-acl='*) 344 | export HALCYON_S3_ACL="${1#*=}";; 345 | '--no-private-storage') 346 | export HALCYON_NO_PRIVATE_STORAGE=1;; 347 | '--no-upload') 348 | export HALCYON_NO_UPLOAD=1;; 349 | '--no-clean-private-storage') 350 | export HALCYON_NO_CLEAN_PRIVATE_STORAGE=1;; 351 | 352 | # GHC options 353 | '--ghc-version') 354 | shift 355 | local ghc_version 356 | expect_args ghc_version -- "$@" 357 | export HALCYON_GHC_VERSION="${ghc_version}";; 358 | '--ghc-version='*) 359 | export HALCYON_GHC_VERSION="${1#*=}";; 360 | '--ghc-pre-build-hook') 361 | shift 362 | local ghc_pre_build_hook 363 | expect_args ghc_pre_build_hook -- "$@" 364 | export HALCYON_GHC_PRE_BUILD_HOOK="${ghc_pre_build_hook}";; 365 | '--ghc-pre-build-hook='*) 366 | export HALCYON_GHC_PRE_BUILD_HOOK="${1#*=}";; 367 | '--ghc-post-build-hook') 368 | shift 369 | local ghc_post_build_hook 370 | expect_args ghc_post_build_hook -- "$@" 371 | export HALCYON_GHC_POST_BUILD_HOOK="${ghc_post_build_hook}";; 372 | '--ghc-post-build-hook='*) 373 | export HALCYON_GHC_POST_BUILD_HOOK="${1#*=}";; 374 | '--ghc-rebuild') 375 | export HALCYON_GHC_REBUILD=1;; 376 | '--ghc-no-remove-doc') 377 | export HALCYON_GHC_NO_REMOVE_DOC=1;; 378 | '--ghc-no-strip') 379 | export HALCYON_GHC_NO_STRIP=1;; 380 | 381 | # Cabal options 382 | '--cabal-version') 383 | shift 384 | local cabal_version 385 | expect_args cabal_version -- "$@" 386 | export HALCYON_CABAL_VERSION="${cabal_version}";; 387 | '--cabal-version='*) 388 | export HALCYON_CABAL_VERSION="${1#*=}";; 389 | '--cabal-remote-repo') 390 | shift 391 | local cabal_remote_repo 392 | expect_args cabal_remote_repo -- "$@" 393 | export HALCYON_CABAL_REMOTE_REPO="${cabal_remote_repo}";; 394 | '--cabal-remote-repo='*) 395 | export HALCYON_CABAL_REMOTE_REPO="${1#*=}";; 396 | '--cabal-pre-build-hook') 397 | shift 398 | local cabal_pre_build_hook 399 | expect_args cabal_pre_build_hook -- "$@" 400 | export HALCYON_CABAL_PRE_BUILD_HOOK="${cabal_pre_build_hook}";; 401 | '--cabal-pre-build-hook='*) 402 | export HALCYON_CABAL_PRE_BUILD_HOOK="${1#*=}";; 403 | '--cabal-post-build-hook') 404 | shift 405 | local cabal_post_build_hook 406 | expect_args cabal_post_build_hook -- "$@" 407 | export HALCYON_CABAL_POST_BUILD_HOOK="${cabal_post_build_hook}";; 408 | '--cabal-post-build-hook='*) 409 | export HALCYON_CABAL_POST_BUILD_HOOK="${1#*=}";; 410 | '--cabal-pre-update-hook') 411 | shift 412 | local cabal_pre_update_hook 413 | expect_args cabal_pre_update_hook -- "$@" 414 | export HALCYON_CABAL_PRE_UPDATE_HOOK="${cabal_pre_update_hook}";; 415 | '--cabal-pre-update-hook='*) 416 | export HALCYON_CABAL_PRE_UPDATE_HOOK="${1#*=}";; 417 | '--cabal-post-update-hook') 418 | shift 419 | local cabal_post_update_hook 420 | expect_args cabal_post_update_hook -- "$@" 421 | export HALCYON_CABAL_POST_UPDATE_HOOK="${cabal_post_update_hook}";; 422 | '--cabal-post-update-hook='*) 423 | export HALCYON_CABAL_POST_UPDATE_HOOK="${1#*=}";; 424 | '--cabal-rebuild') 425 | export HALCYON_CABAL_REBUILD=1;; 426 | '--cabal-update') 427 | export HALCYON_CABAL_UPDATE=1;; 428 | '--cabal-no-strip') 429 | export HALCYON_CABAL_NO_STRIP=1;; 430 | '--cabal-no-update') 431 | export HALCYON_CABAL_NO_UPDATE=1;; 432 | 433 | # Sandbox options 434 | '--sandbox-extra-configure-flags') 435 | shift 436 | local sandbox_extra_configure_flags 437 | expect_args sandbox_extra_configure_flags -- "$@" 438 | export HALCYON_SANDBOX_EXTRA_CONFIGURE_FLAGS="${sandbox_extra_configure_flags}";; 439 | '--sandbox-extra-configure-flags='*) 440 | export HALCYON_SANDBOX_EXTRA_CONFIGURE_FLAGS="${1#*=}";; 441 | '--sandbox-sources') 442 | shift 443 | local sandbox_sources 444 | expect_args sandbox_sources -- "$@" 445 | export HALCYON_SANDBOX_SOURCES="${sandbox_sources}";; 446 | '--sandbox-sources='*) 447 | export HALCYON_SANDBOX_SOURCES="${1#*=}";; 448 | '--sandbox-extra-apps') 449 | shift 450 | local sandbox_extra_apps 451 | expect_args sandbox_extra_apps -- "$@" 452 | export HALCYON_SANDBOX_EXTRA_APPS="${sandbox_extra_apps}";; 453 | '--sandbox-extra-apps='*) 454 | export HALCYON_SANDBOX_EXTRA_APPS="${1#*=}";; 455 | '--sandbox-extra-apps-constraints') 456 | shift 457 | local sandbox_extra_apps_constraints 458 | expect_args sandbox_extra_apps_constraints -- "$@" 459 | export HALCYON_SANDBOX_EXTRA_APPS_CONSTRAINTS="${sandbox_extra_apps_constraints}";; 460 | '--sandbox-extra-apps-constraints='*) 461 | export HALCYON_SANDBOX_EXTRA_APPS_CONSTRAINTS="${1#*=}";; 462 | '--sandbox-extra-os-packages') 463 | shift 464 | local sandbox_extra_os_packages 465 | expect_args sandbox_extra_os_packages -- "$@" 466 | export HALCYON_SANDBOX_EXTRA_OS_PACKAGES="${sandbox_extra_os_packages}";; 467 | '--sandbox-extra-os-packages='*) 468 | export HALCYON_SANDBOX_EXTRA_OS_PACKAGES="${1#*=}";; 469 | '--sandbox-pre-build-hook') 470 | shift 471 | local sandbox_pre_build_hook 472 | expect_args sandbox_pre_build_hook -- "$@" 473 | export HALCYON_SANDBOX_PRE_BUILD_HOOK="${sandbox_pre_build_hook}";; 474 | '--sandbox-pre-build-hook='*) 475 | export HALCYON_SANDBOX_PRE_BUILD_HOOK="${1#*=}";; 476 | '--sandbox-post-build-hook') 477 | shift 478 | local sandbox_post_build_hook 479 | expect_args sandbox_post_build_hook -- "$@" 480 | export HALCYON_SANDBOX_POST_BUILD_HOOK="${sandbox_post_build_hook}";; 481 | '--sandbox-post-build-hook='*) 482 | export HALCYON_SANDBOX_POST_BUILD_HOOK="${1#*=}";; 483 | '--sandbox-rebuild') 484 | export HALCYON_SANDBOX_REBUILD=1;; 485 | '--sandbox-no-remove-doc') 486 | export HALCYON_SANDBOX_NO_REMOVE_DOC=1;; 487 | '--sandbox-no-strip') 488 | export HALCYON_SANDBOX_NO_STRIP=1;; 489 | 490 | '-h'|'--help') 491 | help_usage 492 | return 0 493 | ;; 494 | '--') 495 | shift 496 | while (( $# )); do 497 | if [[ -z "${cmd}" ]]; then 498 | cmd="$1" 499 | else 500 | args_a+=( "$1" ) 501 | fi 502 | shift 503 | done 504 | break 505 | ;; 506 | '-'*) 507 | log_error "Unexpected option: $1" 508 | log 509 | help_usage 510 | return 1 511 | ;; 512 | *) 513 | if [[ -z "${cmd}" ]]; then 514 | cmd="$1" 515 | else 516 | args_a+=( "$1" ) 517 | fi 518 | esac 519 | shift 520 | done 521 | 522 | export HALCYON_INTERNAL_COMMAND="${cmd}" 523 | 524 | if (( HALCYON_LOG_TIMESTAMP )); then 525 | export BASHMENOT_LOG_TIMESTAMP=1 526 | export BASHMENOT_TIMESTAMP_EPOCH=$( get_current_time ) 527 | fi 528 | 529 | # NOTE: HALCYON_CACHE must not be /tmp, as the cache cleaning 530 | # functionality will get confused. 531 | if [[ "${HALCYON_CACHE}" == '/tmp' ]]; then 532 | export HALCYON_CACHE='/tmp/halcyon-cache' 533 | fi 534 | 535 | export BASHMENOT_APT_DIR="${HALCYON_CACHE}/apt" 536 | 537 | export BASHMENOT_AWS_ACCESS_KEY_ID="${HALCYON_AWS_ACCESS_KEY_ID}" 538 | export BASHMENOT_AWS_SECRET_ACCESS_KEY="${HALCYON_AWS_SECRET_ACCESS_KEY}" 539 | export BASHMENOT_S3_ENDPOINT="${HALCYON_S3_ENDPOINT}" 540 | 541 | local tmp_dir 542 | tmp_dir='' 543 | if [[ -z "${BASHMENOT_INTERNAL_TMP:-}" ]]; then 544 | tmp_dir=$( get_tmp_dir 'halcyon' ) || return 1 545 | 546 | if ! mkdir -p "${tmp_dir}"; then 547 | log_error 'Failed to create temporary directory' 548 | return 1 549 | fi 550 | 551 | export BASHMENOT_INTERNAL_TMP="${tmp_dir}" 552 | fi 553 | 554 | # NOTE: Returns 2 if build is needed. 555 | local status 556 | status=0 557 | case "${HALCYON_INTERNAL_COMMAND}" in 558 | 'install'|'build') 559 | halcyon_install ${args_a[@]:+"${args_a[@]}"} || status="$?" 560 | ;; 561 | 'label'|'executable'|'constraints'|'tag') 562 | HALCYON_NO_CLEAN_CACHE=1 \ 563 | halcyon_install ${args_a[@]:+"${args_a[@]}"} || status="$?" 564 | ;; 565 | 'paths') 566 | echo -e "export HALCYON_DIR='${HALCYON_DIR}'" 567 | echo -e "export HALCYON_INTERNAL_PLATFORM='${HALCYON_INTERNAL_PLATFORM}'\n" 568 | 569 | if ! cat "${HALCYON_DIR}/src/paths.sh"; then 570 | log_error 'Failed to export paths' 571 | status=1 572 | fi 573 | ;; 574 | 'help') 575 | help_usage 576 | ;; 577 | '') 578 | log_error 'Expected command' 579 | log 580 | help_usage 581 | status=1 582 | ;; 583 | *) 584 | log_error "Unexpected command: ${cmd} ${args_a[*]:-}" 585 | log 586 | help_usage 587 | status=1 588 | esac 589 | 590 | if ! (( HALCYON_INTERNAL_NO_CLEANUP )) && [[ -n "${tmp_dir}" ]]; then 591 | rm -rf "${tmp_dir}" || true 592 | fi 593 | 594 | return "${status}" 595 | } 596 | -------------------------------------------------------------------------------- /src/core.sh: -------------------------------------------------------------------------------- 1 | detect_package () { 2 | local source_dir 3 | expect_args source_dir -- "$@" 4 | 5 | expect_existing "${source_dir}" || return 1 6 | 7 | local package_file 8 | package_file=$( 9 | find "${source_dir}" -maxdepth 1 -type f -name '*.cabal' | 10 | match_exactly_one 11 | ) || return 1 12 | 13 | cat "${package_file}" 14 | } 15 | 16 | 17 | detect_label () { 18 | local source_dir 19 | expect_args source_dir -- "$@" 20 | 21 | local package 22 | package=$( detect_package "${source_dir}" ) || return 1 23 | 24 | local name 25 | name=$( 26 | awk '/^ *[Nn]ame:/ { print $2 }' <<<"${package}" | 27 | tr -d '\r' | 28 | match_exactly_one 29 | ) || return 1 30 | 31 | local version 32 | version=$( 33 | awk '/^ *[Vv]ersion:/ { print $2 }' <<<"${package}" | 34 | tr -d '\r' | 35 | match_exactly_one 36 | ) || return 1 37 | 38 | echo "${name}-${version}" 39 | } 40 | 41 | 42 | detect_executable () { 43 | local source_dir 44 | expect_args source_dir -- "$@" 45 | 46 | local executable 47 | executable=$( 48 | detect_package "${source_dir}" | 49 | awk '/^ *[Ee]xecutable / { print $2 }' | 50 | tr -d '\r' | 51 | match_at_least_one | 52 | filter_first 53 | ) || return 1 54 | 55 | echo "${executable}" 56 | } 57 | 58 | 59 | determine_ghc_version () { 60 | expect_vars HALCYON_GHC_VERSION 61 | 62 | local constraints 63 | expect_args constraints -- "$@" 64 | 65 | local ghc_version 66 | ghc_version='' 67 | if [[ -n "${constraints}" ]]; then 68 | ghc_version=$( map_constraints_to_ghc_version "${constraints}" ) || return 1 69 | fi 70 | if [[ -z "${ghc_version}" ]]; then 71 | ghc_version="${HALCYON_GHC_VERSION}" 72 | fi 73 | 74 | echo "${ghc_version}" 75 | } 76 | 77 | 78 | determine_ghc_magic_hash () { 79 | local source_dir 80 | expect_args source_dir -- "$@" 81 | 82 | local ghc_magic_hash 83 | if [[ -n "${HALCYON_INTERNAL_GHC_MAGIC_HASH:+_}" ]]; then 84 | ghc_magic_hash="${HALCYON_INTERNAL_GHC_MAGIC_HASH}" 85 | else 86 | ghc_magic_hash=$( hash_ghc_magic "${source_dir}" ) || return 1 87 | fi 88 | 89 | echo "${ghc_magic_hash}" 90 | } 91 | 92 | 93 | determine_cabal_version () { 94 | expect_vars HALCYON_DEFAULT_CABAL_VERSION 95 | 96 | local source_dir 97 | expect_args source_dir -- "$@" 98 | 99 | local cabal_version 100 | cabal_version="${HALCYON_CABAL_VERSION}" 101 | if [[ -z "${cabal_version}" && -f "${source_dir}/.halcyon/cabal-version" ]]; then 102 | cabal_version=$( <"${source_dir}/.halcyon/cabal-version" ) || true 103 | fi 104 | if [[ -z "${cabal_version}" ]]; then 105 | cabal_version="${HALCYON_DEFAULT_CABAL_VERSION}" 106 | fi 107 | 108 | echo "${cabal_version}" 109 | } 110 | 111 | 112 | determine_cabal_magic_hash () { 113 | local source_dir 114 | expect_args source_dir -- "$@" 115 | 116 | local cabal_magic_hash 117 | if [[ -n "${HALCYON_INTERNAL_CABAL_MAGIC_HASH:+_}" ]]; then 118 | cabal_magic_hash="${HALCYON_INTERNAL_CABAL_MAGIC_HASH}" 119 | else 120 | cabal_magic_hash=$( hash_cabal_magic "${source_dir}" ) || return 1 121 | fi 122 | 123 | echo "${cabal_magic_hash}" 124 | } 125 | 126 | 127 | determine_cabal_remote_repo () { 128 | expect_vars HALCYON_DEFAULT_CABAL_REMOTE_REPO 129 | 130 | local source_dir 131 | expect_args source_dir -- "$@" 132 | 133 | local cabal_remote_repo 134 | cabal_remote_repo="${HALCYON_CABAL_REMOTE_REPO}" 135 | if [[ -z "${cabal_remote_repo}" && -f "${source_dir}/.halcyon/cabal-remote-repo" ]]; then 136 | cabal_remote_repo=$( <"${source_dir}/.halcyon/cabal-remote-repo" ) || true 137 | fi 138 | if [[ -z "${cabal_remote_repo}" ]]; then 139 | cabal_remote_repo="${HALCYON_DEFAULT_CABAL_REMOTE_REPO}" 140 | fi 141 | 142 | echo "${cabal_remote_repo}" 143 | } 144 | 145 | 146 | describe_extra () { 147 | local extra_label extra_file 148 | expect_args extra_label extra_file -- "$@" 149 | 150 | if [[ ! -f "${extra_file}" ]]; then 151 | return 0 152 | fi 153 | 154 | local only_first extra 155 | only_first="${extra_label}" 156 | while read -r extra; do 157 | log_indent_label "${only_first}" "${extra}" 158 | only_first='' 159 | done <"${extra_file}" || return 0 160 | } 161 | 162 | 163 | hash_source () { 164 | local source_dir 165 | expect_args source_dir -- "$@" 166 | 167 | # NOTE: Ignoring the same files as in prepare_build_dir. 168 | local -a opts_a 169 | opts_a=() 170 | opts_a+=( \( -name '.git' ) 171 | opts_a+=( -o -name '.gitmodules' ) 172 | opts_a+=( -o -name '.ghc' ) 173 | opts_a+=( -o -name '.cabal' ) 174 | opts_a+=( -o -name '.cabal-sandbox' ) 175 | opts_a+=( -o -name 'cabal.sandbox.config' ) 176 | if [[ -f "${source_dir}/.halcyon/extra-source-hash-ignore" ]]; then 177 | local ignore 178 | while read -r ignore; do 179 | opts_a+=( -o -name "${ignore}" ) 180 | done <"${source_dir}/.halcyon/extra-source-hash-ignore" 181 | fi 182 | opts_a+=( \) -prune -o ) 183 | 184 | local source_hash 185 | if ! source_hash=$( hash_tree "${source_dir}" "${opts_a[@]}" ); then 186 | log_error 'Failed to hash source files' 187 | return 1 188 | fi 189 | 190 | echo "${source_hash}" 191 | } 192 | 193 | 194 | hash_magic () { 195 | local source_dir 196 | expect_args source_dir -- "$@" 197 | 198 | local magic_hash 199 | if ! magic_hash=$( hash_tree "${source_dir}/.halcyon" ); then 200 | log_error 'Failed to hash magic files' 201 | return 1 202 | fi 203 | 204 | echo "${magic_hash}" 205 | } 206 | 207 | 208 | announce_install () { 209 | expect_vars HALCYON_NO_APP HALCYON_DEPENDENCIES_ONLY \ 210 | HALCYON_INTERNAL_NO_ANNOUNCE_INSTALL 211 | 212 | local tag 213 | expect_args tag -- "$@" 214 | 215 | if (( HALCYON_INTERNAL_NO_ANNOUNCE_INSTALL )); then 216 | return 0 217 | fi 218 | 219 | if (( HALCYON_NO_APP )); then 220 | log_label 'GHC and Cabal installed' 221 | return 0 222 | fi 223 | 224 | local thing label 225 | if (( HALCYON_DEPENDENCIES_ONLY )); then 226 | thing='Dependencies' 227 | else 228 | thing='App' 229 | fi 230 | label=$( get_tag_label "${tag}" ) 231 | 232 | case "${HALCYON_INTERNAL_COMMAND}" in 233 | 'install') 234 | log 235 | log_label "${thing} installed:" "${label}" 236 | ;; 237 | 'build') 238 | log 239 | log_label "${thing} built:" "${label}" 240 | esac 241 | } 242 | 243 | 244 | do_install_ghc_and_cabal_dirs () { 245 | expect_vars HALCYON_INTERNAL_RECURSIVE 246 | 247 | local tag source_dir 248 | expect_args tag source_dir -- "$@" 249 | 250 | if (( HALCYON_INTERNAL_RECURSIVE )); then 251 | if ! validate_ghc_dir "${tag}" >'/dev/null' || 252 | ! validate_updated_cabal_dir "${tag}" >'/dev/null' 253 | then 254 | log_error 'Cannot use existing GHC and Cabal directories' 255 | return 1 256 | fi 257 | return 0 258 | fi 259 | 260 | # NOTE: Returns 2 if build is needed. 261 | install_ghc_dir "${tag}" "${source_dir}" || return 262 | log 263 | install_cabal_dir "${tag}" "${source_dir}" || return 264 | log 265 | } 266 | 267 | 268 | install_ghc_and_cabal_dirs () { 269 | expect_vars HALCYON_GHC_VERSION \ 270 | HALCYON_INTERNAL_RECURSIVE 271 | 272 | local source_dir 273 | expect_args source_dir -- "$@" 274 | 275 | local ghc_version ghc_major ghc_minor 276 | ghc_version="${HALCYON_GHC_VERSION}" 277 | ghc_major="${ghc_version%%.*}" 278 | ghc_minor="${ghc_version#*.}" 279 | ghc_minor="${ghc_minor%%.*}" 280 | 281 | local cabal_version cabal_major cabal_minor 282 | cabal_version=$( determine_cabal_version "${source_dir}" ) || return 1 283 | cabal_major="${cabal_version%%.*}" 284 | cabal_minor="${cabal_version#*.}" 285 | cabal_minor="${cabal_minor%%.*}" 286 | 287 | # NOTE: GHC 7.10.* requires Cabal 1.22.0.0 or newer. 288 | if (( ((ghc_major == 7 && ghc_minor >= 10) || ghc_major > 7) && cabal_major == 1 && cabal_minor < 22 )); then 289 | log_error 'Unsupported Cabal version' 290 | log_error 'To use GHC 7.10.1 or newer, use Cabal 1.22.0.0 or newer' 291 | return 1 292 | fi 293 | 294 | local ghc_magic_hash cabal_magic_hash cabal_remote_repo 295 | ghc_magic_hash=$( determine_ghc_magic_hash "${source_dir}" ) || return 1 296 | cabal_magic_hash=$( determine_cabal_magic_hash "${source_dir}" ) || return 1 297 | cabal_remote_repo=$( determine_cabal_remote_repo "${source_dir}" ) || return 1 298 | 299 | if ! (( HALCYON_INTERNAL_RECURSIVE )); then 300 | log 'Installing GHC and Cabal' 301 | 302 | describe_storage 303 | 304 | log_indent_label 'GHC version:' "${ghc_version}" 305 | [[ -n "${ghc_magic_hash}" ]] && log_indent_label 'GHC magic hash:' "${ghc_magic_hash:0:7}" 306 | 307 | log_indent_label 'Cabal version:' "${cabal_version}" 308 | [[ -n "${cabal_magic_hash}" ]] && log_indent_label 'Cabal magic hash:' "${cabal_magic_hash:0:7}" 309 | log_indent_label 'Cabal remote-repo:' "${cabal_remote_repo}" 310 | log 311 | fi 312 | 313 | local tag 314 | tag=$( 315 | create_tag '' '' '' '' '' \ 316 | "${ghc_version}" "${ghc_magic_hash}" \ 317 | "${cabal_version}" "${cabal_magic_hash}" "${cabal_remote_repo}" '' \ 318 | '' 319 | ) 320 | 321 | # NOTE: Returns 2 if build is needed. 322 | do_install_ghc_and_cabal_dirs "${tag}" "${source_dir}" || return 323 | 324 | announce_install "${tag}" 325 | } 326 | 327 | 328 | do_fast_install_app () { 329 | local tag source_dir 330 | expect_args tag source_dir -- "$@" 331 | 332 | local label install_dir 333 | label=$( get_tag_label "${tag}" ) 334 | install_dir=$( get_tmp_dir "install-${label}" ) || return 1 335 | 336 | restore_install_dir "${tag}" "${install_dir}" || return 1 337 | install_app "${tag}" "${source_dir}" "${install_dir}" || return 1 338 | } 339 | 340 | 341 | fast_install_app () { 342 | expect_vars HALCYON_PREFIX HALCYON_DEPENDENCIES_ONLY HALCYON_KEEP_DEPENDENCIES \ 343 | HALCYON_APP_REBUILD HALCYON_APP_RECONFIGURE HALCYON_APP_REINSTALL \ 344 | HALCYON_GHC_VERSION HALCYON_GHC_REBUILD \ 345 | HALCYON_CABAL_REBUILD HALCYON_CABAL_UPDATE \ 346 | HALCYON_SANDBOX_REBUILD \ 347 | HALCYON_INTERNAL_RECURSIVE 348 | 349 | local label source_hash source_dir 350 | expect_args label source_hash source_dir -- "$@" 351 | 352 | if (( HALCYON_DEPENDENCIES_ONLY )) || (( HALCYON_KEEP_DEPENDENCIES )) || 353 | (( HALCYON_APP_REBUILD )) || (( HALCYON_APP_RECONFIGURE )) || (( HALCYON_APP_REINSTALL )) || 354 | (( HALCYON_GHC_REBUILD )) || 355 | (( HALCYON_CABAL_REBUILD )) || (( HALCYON_CABAL_UPDATE )) || 356 | (( HALCYON_SANDBOX_REBUILD )) 357 | then 358 | return 1 359 | fi 360 | 361 | expect_existing "${source_dir}" || return 1 362 | 363 | log_indent_label 'Label:' "${label}" 364 | log_indent_label 'Prefix:' "${HALCYON_PREFIX}" 365 | log_indent_label 'Source hash:' "${source_hash:0:7}" 366 | 367 | describe_storage 368 | 369 | log_indent_label 'GHC version:' "${HALCYON_GHC_VERSION}" 370 | log 371 | 372 | local tag 373 | tag=$( 374 | create_tag "${HALCYON_PREFIX}" "${label}" "${source_hash}" '' '' \ 375 | "${HALCYON_GHC_VERSION}" '' \ 376 | '' '' '' '' \ 377 | '' 378 | ) 379 | 380 | if ! do_fast_install_app "${tag}" "${source_dir}"; then 381 | log 382 | return 1 383 | fi 384 | 385 | if ! (( HALCYON_INTERNAL_RECURSIVE )); then 386 | announce_install "${tag}" 387 | touch_cached_ghc_and_cabal_files 388 | fi 389 | } 390 | 391 | 392 | prepare_file_option () { 393 | local magic_var magic_file 394 | expect_args magic_var magic_file -- "$@" 395 | 396 | if [[ -z "${magic_var}" ]]; then 397 | return 0 398 | fi 399 | 400 | copy_file "${magic_var}" "${magic_file}" || return 1 401 | } 402 | 403 | 404 | prepare_string_file_option () { 405 | local magic_var magic_file 406 | expect_args magic_var magic_file -- "$@" 407 | 408 | if [[ -z "${magic_var}" ]]; then 409 | return 0 410 | fi 411 | if [[ -f "${magic_var}" ]]; then 412 | copy_file "${magic_var}" "${magic_file}" || return 1 413 | return 0 414 | fi 415 | 416 | local -a string_a 417 | string_a=( ${magic_var} ) 418 | 419 | copy_file <( IFS=$'\n' && echo "${string_a[*]}" ) "${magic_file}" || return 1 420 | } 421 | 422 | 423 | prepare_constraints_option () { 424 | local magic_var magic_file 425 | expect_args magic_var magic_file -- "$@" 426 | 427 | if [[ -z "${magic_var}" ]]; then 428 | return 0 429 | fi 430 | if [[ -d "${magic_var}" ]]; then 431 | copy_dir_over "${magic_var}" "${magic_file}" || return 1 432 | return 0 433 | fi 434 | if [[ -f "${magic_var}" ]]; then 435 | copy_file "${magic_var}" "${magic_file}" || return 1 436 | return 0 437 | fi 438 | 439 | copy_file <( echo "${magic_var}" ) "${magic_file}" || return 1 440 | } 441 | 442 | 443 | prepare_source_dir () { 444 | local label source_dir 445 | expect_args label source_dir -- "$@" 446 | 447 | expect_existing "${source_dir}" || return 1 448 | 449 | # NOTE: Listing executable-only packages in build-tools causes Cabal 450 | # to expect the executables to be installed, but not to install the 451 | # packages. 452 | # Listing executable-only packages in build-depends causes Cabal to 453 | # install the packages, and to fail to recognise the packages have 454 | # been installed. 455 | # https://github.com/haskell/cabal/issues/220 456 | # https://github.com/haskell/cabal/issues/779 457 | local magic_dir 458 | magic_dir="${source_dir}/.halcyon" 459 | 460 | # Build-time magic files 461 | prepare_string_file_option "${HALCYON_EXTRA_SOURCE_HASH_IGNORE}" "${magic_dir}/extra-source-hash-ignore" || return 1 462 | prepare_string_file_option "${HALCYON_EXTRA_CONFIGURE_FLAGS}" "${magic_dir}/extra-configure-flags" || return 1 463 | prepare_file_option "${HALCYON_PRE_BUILD_HOOK}" "${magic_dir}/pre-build-hook" || return 1 464 | prepare_file_option "${HALCYON_POST_BUILD_HOOK}" "${magic_dir}/post-build-hook" || return 1 465 | 466 | # Install-time magic files 467 | prepare_string_file_option "${HALCYON_EXTRA_APPS}" "${magic_dir}/extra-apps" || return 1 468 | prepare_constraints_option "${HALCYON_EXTRA_APPS_CONSTRAINTS}" "${magic_dir}/extra-apps-constraints" || return 1 469 | prepare_string_file_option "${HALCYON_EXTRA_DATA_FILES}" "${magic_dir}/extra-data-files" || return 1 470 | prepare_string_file_option "${HALCYON_EXTRA_OS_PACKAGES}" "${magic_dir}/extra-os-packages" || return 1 471 | prepare_file_option "${HALCYON_PRE_INSTALL_HOOK}" "${magic_dir}/pre-install-hook" || return 1 472 | prepare_file_option "${HALCYON_POST_INSTALL_HOOK}" "${magic_dir}/post-install-hook" || return 1 473 | 474 | # GHC magic files 475 | prepare_file_option "${HALCYON_GHC_PRE_BUILD_HOOK}" "${magic_dir}/ghc-pre-build-hook" || return 1 476 | prepare_file_option "${HALCYON_GHC_POST_BUILD_HOOK}" "${magic_dir}/ghc-post-build-hook" || return 1 477 | 478 | # Cabal magic files 479 | prepare_string_file_option "${HALCYON_CABAL_REMOTE_REPO}" "${magic_dir}/cabal-remote-repo" || return 1 480 | prepare_file_option "${HALCYON_CABAL_PRE_BUILD_HOOK}" "${magic_dir}/cabal-pre-build-hook" || return 1 481 | prepare_file_option "${HALCYON_CABAL_POST_BUILD_HOOK}" "${magic_dir}/cabal-post-build-hook" || return 1 482 | prepare_file_option "${HALCYON_CABAL_PRE_UPDATE_HOOK}" "${magic_dir}/cabal-pre-update-hook" || return 1 483 | prepare_file_option "${HALCYON_CABAL_POST_UPDATE_HOOK}" "${magic_dir}/cabal-post-update-hook" || return 1 484 | 485 | # Sandbox magic files 486 | prepare_string_file_option "${HALCYON_SANDBOX_EXTRA_CONFIGURE_FLAGS}" "${magic_dir}/sandbox-extra-configure-flags" || return 1 487 | prepare_string_file_option "${HALCYON_SANDBOX_SOURCES}" "${magic_dir}/sandbox-sources" || return 1 488 | prepare_string_file_option "${HALCYON_SANDBOX_EXTRA_APPS}" "${magic_dir}/sandbox-extra-apps" || return 1 489 | prepare_constraints_option "${HALCYON_SANDBOX_EXTRA_APPS_CONSTRAINTS}" "${magic_dir}/sandbox-extra-apps-constraints" || return 1 490 | prepare_string_file_option "${HALCYON_SANDBOX_EXTRA_OS_PACKAGES}" "${magic_dir}/sandbox-extra-os-packages" || return 1 491 | prepare_file_option "${HALCYON_SANDBOX_PRE_BUILD_HOOK}" "${magic_dir}/sandbox-pre-build-hook" || return 1 492 | prepare_file_option "${HALCYON_SANDBOX_POST_BUILD_HOOK}" "${magic_dir}/sandbox-post-build-hook" || return 1 493 | } 494 | 495 | 496 | validate_extra_configure_flags () { 497 | local source_dir 498 | expect_args source_dir -- "$@" 499 | 500 | local cabal_version cabal_major cabal_minor 501 | cabal_version=$( determine_cabal_version "${source_dir}" ) || return 1 502 | cabal_major="${cabal_version%%.*}" 503 | cabal_minor="${cabal_version#*.}" 504 | cabal_minor="${cabal_minor%%.*}" 505 | 506 | if [[ -f "${source_dir}/.halcyon/extra-configure-flags" ]]; then 507 | local flag 508 | while read -r flag; do 509 | case "${flag}" in 510 | '--prefix='*) 511 | log_error "Unsupported extra configure flag: ${flag}" 512 | return 1 513 | esac 514 | done <"${source_dir}/.halcyon/extra-configure-flags" || true 515 | fi 516 | 517 | if [[ -f "${source_dir}/.halcyon/sandbox-extra-configure-flags" ]]; then 518 | local flag 519 | while read -r flag; do 520 | case "${flag}" in 521 | '--prefix='*) 522 | log_error "Unsupported sandbox extra configure flag: ${flag}" 523 | return 1 524 | ;; 525 | '--enable-benchmarks'|'--disable-benchmarks'|'--enable-tests'|'--disable-tests') 526 | if (( cabal_major == 1 && cabal_minor < 22 )); then 527 | log_error "Unsupported sandbox extra configure flag for Cabal ${cabal_version}: ${flag}" 528 | log_error "To use ${flag}, use Cabal 1.22.0.0 or newer" 529 | return 1 530 | fi 531 | esac 532 | done <"${source_dir}/.halcyon/sandbox-extra-configure-flags" || true 533 | fi 534 | } 535 | 536 | 537 | do_full_install_app () { 538 | expect_vars HALCYON_BASE HALCYON_DEPENDENCIES_ONLY \ 539 | HALCYON_APP_REBUILD HALCYON_APP_RECONFIGURE HALCYON_APP_REINSTALL \ 540 | HALCYON_SANDBOX_REBUILD \ 541 | HALCYON_INTERNAL_RECURSIVE 542 | 543 | local tag source_dir constraints 544 | expect_args tag source_dir constraints -- "$@" 545 | 546 | local label build_dir install_dir saved_sandbox 547 | label=$( get_tag_label "${tag}" ) 548 | build_dir=$( get_tmp_dir "build-${label}" ) || return 1 549 | install_dir=$( get_tmp_dir "install-${label}" ) || return 1 550 | saved_sandbox='' 551 | 552 | # NOTE: Returns 2 if build is needed. 553 | do_install_ghc_and_cabal_dirs "${tag}" "${source_dir}" || return 554 | 555 | if (( HALCYON_INTERNAL_RECURSIVE )); then 556 | if [[ -d "${HALCYON_BASE}/sandbox" ]]; then 557 | if ! saved_sandbox=$( get_tmp_dir 'saved-sandbox' ) || 558 | ! mv "${HALCYON_BASE}/sandbox" "${saved_sandbox}" 559 | then 560 | log_error 'Failed to put away existing sandbox' 561 | return 1 562 | fi 563 | fi 564 | fi 565 | 566 | # NOTE: Returns 2 if build is needed. 567 | install_sandbox_dir "${tag}" "${source_dir}" "${constraints}" || return 568 | validate_actual_constraints "${tag}" "${source_dir}" "${constraints}" 569 | log 570 | 571 | if ! (( HALCYON_DEPENDENCIES_ONLY )); then 572 | # NOTE: Returns 2 if build is needed. 573 | build_app "${tag}" "${source_dir}" "${build_dir}" || return 574 | fi 575 | 576 | if [[ "${HALCYON_INTERNAL_COMMAND}" == 'install' ]] && 577 | ! (( HALCYON_DEPENDENCIES_ONLY )) 578 | then 579 | log 580 | 581 | if (( HALCYON_APP_REBUILD )) || 582 | (( HALCYON_APP_RECONFIGURE )) || 583 | (( HALCYON_APP_REINSTALL )) || 584 | (( HALCYON_SANDBOX_REBUILD )) || 585 | ! restore_install_dir "${tag}" "${install_dir}" 586 | then 587 | # NOTE: Returns 2 if build is needed. 588 | prepare_install_dir "${tag}" "${source_dir}" "${constraints}" "${build_dir}" "${install_dir}" || return 589 | archive_install_dir "${install_dir}" || return 1 590 | fi 591 | fi 592 | 593 | if (( HALCYON_INTERNAL_RECURSIVE )); then 594 | if ! rm -rf "${HALCYON_BASE}/sandbox"; then 595 | log_error 'Failed to remove sandbox' 596 | return 1 597 | fi 598 | if [[ -n "${saved_sandbox}" ]]; then 599 | if ! mv "${saved_sandbox}" "${HALCYON_BASE}/sandbox"; then 600 | log_error 'Failed to put back existing sandbox' 601 | return 1 602 | fi 603 | fi 604 | fi 605 | 606 | if [[ "${HALCYON_INTERNAL_COMMAND}" == 'install' ]] && 607 | ! (( HALCYON_DEPENDENCIES_ONLY )) 608 | then 609 | install_app "${tag}" "${source_dir}" "${install_dir}" || return 1 610 | fi 611 | } 612 | 613 | 614 | full_install_app () { 615 | expect_vars HALCYON_PREFIX HALCYON_DEPENDENCIES_ONLY \ 616 | HALCYON_INTERNAL_RECURSIVE HALCYON_INTERNAL_TOLERATE_GHC_USER_DB 617 | 618 | local label source_dir 619 | expect_args label source_dir -- "$@" 620 | 621 | expect_existing "${source_dir}" || return 1 622 | 623 | case "${HALCYON_INTERNAL_COMMAND}" in 624 | 'label') 625 | echo "${label}" 626 | return 0 627 | ;; 628 | 'executable') 629 | local executable 630 | if ! executable=$( detect_executable "${source_dir}" ); then 631 | log_error 'Failed to detect executable' 632 | return 1 633 | fi 634 | 635 | echo "${executable}" 636 | return 0 637 | ;; 638 | esac 639 | 640 | log "Installing ${label}" 641 | 642 | # NOTE: First of two places where source_dir is modified. 643 | if ! prepare_constraints "${label}" "${source_dir}" || 644 | ! prepare_source_dir "${label}" "${source_dir}" 645 | then 646 | log_error 'Failed to prepare source directory' 647 | return 1 648 | fi 649 | 650 | validate_extra_configure_flags "${source_dir}" || return 1 651 | 652 | local source_hash 653 | if [[ -f "${source_dir}/cabal.config" ]]; then 654 | source_hash=$( hash_source "${source_dir}" ) || return 1 655 | 656 | if [[ "${HALCYON_INTERNAL_COMMAND}" == 'install' ]] && 657 | fast_install_app "${label}" "${source_hash}" "${source_dir}" 658 | then 659 | return 0 660 | fi 661 | fi 662 | 663 | local constraints 664 | constraints='' 665 | if [[ -f "${source_dir}/cabal.config" ]]; then 666 | log 'Determining constraints' 667 | 668 | if ! constraints=$( detect_constraints "${label}" "${source_dir}" ); then 669 | log_error 'Failed to determine constraints' 670 | return 1 671 | fi 672 | fi 673 | if [[ -z "${constraints}" ]]; then 674 | # NOTE: Returns 2 if build is needed. 675 | HALCYON_GHC_REBUILD=0 \ 676 | HALCYON_CABAL_REBUILD=0 HALCYON_CABAL_UPDATE=0 \ 677 | HALCYON_INTERNAL_NO_ANNOUNCE_INSTALL=1 \ 678 | install_ghc_and_cabal_dirs "${source_dir}" || return 679 | 680 | log 'Determining constraints' 681 | 682 | if ! constraints=$( cabal_determine_constraints "${label}" "${source_dir}" ); then 683 | log_error 'Failed to determine constraints' 684 | return 1 685 | fi 686 | 687 | local cabal_remote_repo 688 | cabal_remote_repo=$( determine_cabal_remote_repo "${source_dir}" ) || return 1 689 | if ! is_stackage "${cabal_remote_repo}"; then 690 | log_warning 'Using newest versions of all packages' 691 | if [[ "${HALCYON_INTERNAL_COMMAND}" != 'constraints' ]]; then 692 | format_constraints <<<"${constraints}" | quote 693 | log 694 | fi 695 | fi 696 | 697 | # NOTE: Second of two places where source_dir is modified. 698 | if ! format_constraints_to_cabal_freeze <<<"${constraints}" >"${source_dir}/cabal.config"; then 699 | log_error 'Failed to write Cabal config' 700 | return 1 701 | fi 702 | 703 | source_hash=$( hash_source "${source_dir}" ) || return 1 704 | 705 | if [[ "${HALCYON_INTERNAL_COMMAND}" == 'install' ]] && 706 | fast_install_app "${label}" "${source_hash}" "${source_dir}" 707 | then 708 | return 0 709 | fi 710 | fi 711 | if [[ "${HALCYON_INTERNAL_COMMAND}" == 'constraints' ]]; then 712 | format_constraints <<<"${constraints}" 713 | return 0 714 | fi 715 | 716 | local constraints_hash magic_hash 717 | constraints_hash=$( hash_constraints "${constraints}" ) || return 1 718 | magic_hash=$( hash_magic "${source_dir}" ) || return 1 719 | 720 | local ghc_version ghc_magic_hash 721 | ghc_version=$( determine_ghc_version "${constraints}" ) || return 1 722 | ghc_magic_hash=$( determine_ghc_magic_hash "${source_dir}" ) || return 1 723 | 724 | local cabal_version cabal_magic_hash cabal_remote_repo 725 | cabal_version=$( determine_cabal_version "${source_dir}" ) || return 1 726 | cabal_magic_hash=$( determine_cabal_magic_hash "${source_dir}" ) || return 1 727 | cabal_remote_repo=$( determine_cabal_remote_repo "${source_dir}" ) || return 1 728 | 729 | local sandbox_magic_hash 730 | sandbox_magic_hash=$( hash_sandbox_magic "${source_dir}" ) || return 1 731 | 732 | log_indent_label 'Label:' "${label}" 733 | log_indent_label 'Prefix:' "${HALCYON_PREFIX}" 734 | log_indent_label 'Source hash:' "${source_hash:0:7}" 735 | 736 | describe_extra 'Extra source hash ignore:' "${source_dir}/.halcyon/extra-source-hash-ignore" 737 | log_indent_label 'Constraints hash:' "${constraints_hash:0:7}" 738 | describe_extra 'Extra configure flags:' "${source_dir}/.halcyon/extra-configure-flags" 739 | describe_extra 'Extra apps:' "${source_dir}/.halcyon/extra-apps" 740 | describe_extra 'Extra data files:' "${source_dir}/.halcyon/extra-data-files" 741 | describe_extra 'Extra OS packages:' "${source_dir}/.halcyon/extra-os-packages" 742 | [[ -n "${magic_hash}" ]] && log_indent_label 'Magic hash:' "${magic_hash:0:7}" 743 | 744 | describe_storage 745 | 746 | log_indent_label 'GHC version:' "${ghc_version}" 747 | [[ -n "${ghc_magic_hash}" ]] && log_indent_label 'GHC magic hash:' "${ghc_magic_hash:0:7}" 748 | 749 | log_indent_label 'Cabal version:' "${cabal_version}" 750 | [[ -n "${cabal_magic_hash}" ]] && log_indent_label 'Cabal magic hash:' "${cabal_magic_hash:0:7}" 751 | log_indent_label 'Cabal remote-repo:' "${cabal_remote_repo}" 752 | 753 | [[ -n "${sandbox_magic_hash}" ]] && log_indent_label 'Sandbox magic hash:' "${sandbox_magic_hash:0:7}" 754 | describe_extra 'Sandbox extra configure flags:' "${source_dir}/.halcyon/sandbox-extra-configure-flags" 755 | describe_extra 'Sandbox sources:' "${source_dir}/.halcyon/sandbox-sources" 756 | describe_extra 'Sandbox extra apps:' "${source_dir}/.halcyon/sandbox-extra-apps" 757 | describe_extra 'Sandbox extra OS packages:' "${source_dir}/.halcyon/sandbox-extra-os-packages" 758 | 759 | # NOTE: In some circumstances, Cabal can break sandbox isolation. 760 | # https://github.com/haskell/cabal/issues/2400 761 | if ! (( HALCYON_INTERNAL_TOLERATE_GHC_USER_DB )) && 762 | find_tree ~/.ghc \( -name 'ghci_history' \) -prune -o -type f -print | 763 | match_at_least_one >'/dev/null' 764 | then 765 | log_warning 'Unexpected GHC user package database' 766 | log_warning 'https://github.com/haskell/cabal/issues/2400' 767 | fi 768 | 769 | local tag 770 | tag=$( 771 | create_tag "${HALCYON_PREFIX}" "${label}" "${source_hash}" "${constraints_hash}" "${magic_hash}" \ 772 | "${ghc_version}" "${ghc_magic_hash}" \ 773 | "${cabal_version}" "${cabal_magic_hash}" "${cabal_remote_repo}" '' \ 774 | "${sandbox_magic_hash}" 775 | ) 776 | 777 | if [[ "${HALCYON_INTERNAL_COMMAND}" == 'tag' ]]; then 778 | echo "${tag}" 779 | return 0 780 | fi 781 | 782 | # NOTE: Returns 2 if build is needed. 783 | log 784 | do_full_install_app "${tag}" "${source_dir}" "${constraints}" || return 785 | 786 | if ! (( HALCYON_INTERNAL_RECURSIVE )); then 787 | announce_install "${tag}" 788 | fi 789 | } 790 | 791 | 792 | install_local_app () { 793 | expect_vars HALCYON_INTERNAL_NO_COPY_LOCAL_SOURCE 794 | 795 | local local_dir 796 | expect_args local_dir -- "$@" 797 | 798 | local label 799 | if ! label=$( detect_label "${local_dir}" ); then 800 | log_error 'Failed to detect app' 801 | return 1 802 | fi 803 | 804 | if (( HALCYON_INTERNAL_NO_COPY_LOCAL_SOURCE )); then 805 | # NOTE: Returns 2 if build is needed. 806 | full_install_app "${label}" "${local_dir}" || return 807 | return 0 808 | fi 809 | 810 | local source_dir 811 | source_dir=$( get_tmp_dir "source-${label}" ) || return 1 812 | 813 | copy_dir_over "${local_dir}" "${source_dir}" || return 1 814 | 815 | # NOTE: Returns 2 if build is needed. 816 | full_install_app "${label}" "${source_dir}" || return 817 | } 818 | 819 | 820 | install_cloned_app () { 821 | local url 822 | expect_args url -- "$@" 823 | 824 | local clone_dir 825 | clone_dir=$( get_tmp_dir 'git-clone' ) || return 1 826 | 827 | log_begin "Cloning ${url}..." 828 | 829 | local commit_hash 830 | if ! commit_hash=$( git_clone_over "${url}" "${clone_dir}" ); then 831 | log_end 'error' 832 | return 1 833 | fi 834 | log_end "done, ${commit_hash}" 835 | 836 | local label 837 | if ! label=$( detect_label "${clone_dir}" ); then 838 | log_error 'Failed to detect app' 839 | return 1 840 | fi 841 | 842 | local source_dir 843 | source_dir=$( get_tmp_dir "source-${label}" ) || return 1 844 | 845 | mv "${clone_dir}" "${source_dir}" || return 1 846 | 847 | # NOTE: Returns 2 if build is needed. 848 | HALCYON_INTERNAL_REMOTE_SOURCE=1 \ 849 | full_install_app "${label}" "${source_dir}" || return 850 | } 851 | 852 | 853 | install_unpacked_app () { 854 | local thing 855 | expect_args thing -- "$@" 856 | 857 | local unpack_dir 858 | unpack_dir=$( get_tmp_dir 'cabal-unpack' ) || return 1 859 | 860 | # NOTE: Returns 2 if build is needed. 861 | HALCYON_NO_APP=1 \ 862 | HALCYON_GHC_REBUILD=0 \ 863 | HALCYON_CABAL_REBUILD=0 HALCYON_CABAL_UPDATE=0 \ 864 | HALCYON_INTERNAL_NO_ANNOUNCE_INSTALL=1 \ 865 | install_ghc_and_cabal_dirs '/dev/null' || return 866 | 867 | log 'Unpacking app' 868 | 869 | local label 870 | if ! label=$( cabal_unpack_over "${thing}" "${unpack_dir}" ); then 871 | log_error 'Failed to unpack app' 872 | return 1 873 | fi 874 | 875 | local cabal_remote_repo 876 | cabal_remote_repo=$( determine_cabal_remote_repo "${unpack_dir}/${label}" ) 877 | if ! is_stackage "${cabal_remote_repo}"; then 878 | if [[ "${label}" != "${thing}" ]]; then 879 | log_warning "Using newest version of ${thing}: ${label}" 880 | fi 881 | fi 882 | 883 | local source_dir 884 | source_dir=$( get_tmp_dir "source-${label}" ) || return 1 885 | 886 | mv "${unpack_dir}/${label}" "${source_dir}" || return 1 887 | 888 | # NOTE: Returns 2 if build is needed. 889 | HALCYON_INTERNAL_REMOTE_SOURCE=1 \ 890 | full_install_app "${label}" "${source_dir}" || return 891 | } 892 | 893 | 894 | halcyon_install () { 895 | expect_vars HALCYON_NO_APP 896 | 897 | if (( BASH_VERSINFO[0] < 4 )); then 898 | log_error "Unsupported GNU bash version: ${BASH_VERSION}" 899 | log_error 'To use Halcyon, use GNU bash 4 or newer' 900 | return 1 901 | fi 902 | 903 | if (( $# > 1 )); then 904 | shift 905 | log_error "Unexpected args: $*" 906 | return 1 907 | fi 908 | 909 | local cache_dir 910 | cache_dir=$( get_tmp_dir 'cache' ) || return 1 911 | 912 | if ! prepare_cache "${cache_dir}"; then 913 | log_error 'Failed to prepare cache' 914 | return 1 915 | fi 916 | 917 | # NOTE: Returns 2 if build is needed. 918 | if (( HALCYON_NO_APP )); then 919 | install_ghc_and_cabal_dirs '/dev/null' || return 920 | elif ! (( $# )); then 921 | if ! detect_label '.' >'/dev/null'; then 922 | HALCYON_NO_APP=1 \ 923 | install_ghc_and_cabal_dirs '/dev/null' || return 924 | else 925 | install_local_app '.' || return 926 | fi 927 | else 928 | if validate_git_url "$1"; then 929 | install_cloned_app "$1" || return 930 | elif [[ -d "$1" ]]; then 931 | install_local_app "${1%/}" || return 932 | else 933 | install_unpacked_app "$1" || return 934 | fi 935 | fi 936 | 937 | if ! clean_cache "${cache_dir}"; then 938 | log_warning 'Failed to clean cache' 939 | fi 940 | } 941 | -------------------------------------------------------------------------------- /src/cabal.sh: -------------------------------------------------------------------------------- 1 | map_cabal_version_to_original_url () { 2 | local cabal_version 3 | expect_args cabal_version -- "$@" 4 | 5 | case "${cabal_version}" in 6 | '1.20.0.0') echo 'https://haskell.org/cabal/release/cabal-install-1.20.0.0/cabal-install-1.20.0.0.tar.gz';; 7 | '1.20.0.1') echo 'https://haskell.org/cabal/release/cabal-install-1.20.0.1/cabal-install-1.20.0.1.tar.gz';; 8 | '1.20.0.2') echo 'https://haskell.org/cabal/release/cabal-install-1.20.0.2/cabal-install-1.20.0.2.tar.gz';; 9 | '1.20.0.3') echo 'https://haskell.org/cabal/release/cabal-install-1.20.0.3/cabal-install-1.20.0.3.tar.gz';; 10 | '1.20.0.5') echo 'https://haskell.org/cabal/release/cabal-install-1.20.0.5/cabal-install-1.20.0.5.tar.gz';; 11 | '1.20.0.6') echo 'https://haskell.org/cabal/release/cabal-install-1.20.0.6/cabal-install-1.20.0.6.tar.gz';; 12 | '1.20.1.0') echo 'https://haskell.org/cabal/release/cabal-install-1.20.1.0/cabal-install-1.20.1.0.tar.gz';; 13 | '1.22.0.0') echo 'https://haskell.org/cabal/release/cabal-install-1.22.0.0/cabal-install-1.22.0.0.tar.gz';; 14 | '1.22.0.1') echo 'https://github.com/haskell/cabal/archive/cabal-install-v1.22.0.1.tar.gz';; 15 | '1.22.2.0') echo 'https://haskell.org/cabal/release/cabal-install-1.22.2.0/cabal-install-1.22.2.0.tar.gz';; 16 | '1.22.3.0') echo 'https://haskell.org/cabal/release/cabal-install-1.22.3.0/cabal-install-1.22.3.0.tar.gz';; 17 | '1.22.4.0') echo 'https://haskell.org/cabal/release/cabal-install-1.22.4.0/cabal-install-1.22.4.0.tar.gz';; 18 | '1.22.5.0') echo 'https://haskell.org/cabal/release/cabal-install-1.22.5.0/cabal-install-1.22.5.0.tar.gz';; 19 | '1.22.6.0') echo 'https://haskell.org/cabal/release/cabal-install-1.22.6.0/cabal-install-1.22.6.0.tar.gz';; 20 | *) 21 | # NOTE: Bootstrapping cabal-install 1.20.0.4 does not work. 22 | # https://www.haskell.org/pipermail/cabal-devel/2014-December/009959.html 23 | log_error "Unsupported Cabal version: ${cabal_version}" 24 | return 1 25 | esac 26 | } 27 | 28 | 29 | is_stackage () { 30 | local cabal_remote_repo 31 | expect_args cabal_remote_repo -- "$@" 32 | 33 | case "${cabal_remote_repo}" in 34 | 'stackage-'*) return 0;; 35 | *) return 1 36 | esac 37 | } 38 | 39 | 40 | create_cabal_tag () { 41 | local cabal_version cabal_magic_hash cabal_remote_repo cabal_date 42 | expect_args cabal_version cabal_magic_hash cabal_remote_repo cabal_date -- "$@" 43 | 44 | create_tag '' '' '' '' '' \ 45 | '' '' \ 46 | "${cabal_version}" "${cabal_magic_hash}" "${cabal_remote_repo}" "${cabal_date}" \ 47 | '' 48 | } 49 | 50 | 51 | detect_cabal_tag () { 52 | local tag_file 53 | expect_args tag_file -- "$@" 54 | 55 | local tag_pattern 56 | tag_pattern=$( create_cabal_tag '.*' '.*' '.*' '.*' ) 57 | 58 | local tag 59 | if ! tag=$( detect_tag "${tag_file}" "${tag_pattern}" ); then 60 | log_error 'Failed to detect Cabal tag' 61 | return 1 62 | fi 63 | 64 | echo "${tag}" 65 | } 66 | 67 | 68 | derive_base_cabal_tag () { 69 | local tag 70 | expect_args tag -- "$@" 71 | 72 | local cabal_version cabal_magic_hash 73 | cabal_version=$( get_tag_cabal_version "${tag}" ) 74 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 75 | 76 | create_cabal_tag "${cabal_version}" "${cabal_magic_hash}" '' '' 77 | } 78 | 79 | 80 | derive_updated_cabal_tag () { 81 | local tag 82 | expect_args tag -- "$@" 83 | 84 | local cabal_version cabal_magic_hash cabal_remote_repo cabal_date 85 | cabal_version=$( get_tag_cabal_version "${tag}" ) 86 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 87 | cabal_remote_repo=$( get_tag_cabal_remote_repo "${tag}" ) 88 | cabal_date='' 89 | if ! is_stackage "${cabal_remote_repo}"; then 90 | cabal_date=$( get_date '+%Y-%m-%d' ) 91 | fi 92 | 93 | create_cabal_tag "${cabal_version}" "${cabal_magic_hash}" "${cabal_remote_repo}" "${cabal_date}" 94 | } 95 | 96 | 97 | derive_updated_cabal_tag_pattern () { 98 | local tag 99 | expect_args tag -- "$@" 100 | 101 | local cabal_version cabal_magic_hash cabal_remote_repo 102 | cabal_version=$( get_tag_cabal_version "${tag}" ) 103 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 104 | cabal_remote_repo=$( get_tag_cabal_remote_repo "${tag}" ) 105 | 106 | create_cabal_tag "${cabal_version//./\.}" "${cabal_magic_hash}" "${cabal_remote_repo//.\.}" '.*' 107 | } 108 | 109 | 110 | format_cabal_id () { 111 | local tag 112 | expect_args tag -- "$@" 113 | 114 | local cabal_version cabal_magic_hash 115 | cabal_version=$( get_tag_cabal_version "${tag}" ) 116 | cabal_magic_hash=$( get_tag_cabal_magic_hash "${tag}" ) 117 | 118 | echo "${cabal_version}${cabal_magic_hash:+.${cabal_magic_hash:0:7}}" 119 | } 120 | 121 | 122 | format_cabal_remote_repo_name () { 123 | local tag 124 | expect_args tag -- "$@" 125 | 126 | local cabal_remote_repo 127 | cabal_remote_repo=$( get_tag_cabal_remote_repo "${tag}" ) 128 | 129 | echo "${cabal_remote_repo%%:*}" 130 | } 131 | 132 | 133 | format_cabal_config () { 134 | expect_vars HALCYON_BASE 135 | 136 | local tag 137 | expect_args tag -- "$@" 138 | 139 | # NOTE: Cabal does not support HTTPS repository URLs. 140 | # https://github.com/haskell/cabal/issues/936 141 | local cabal_remote_repo 142 | cabal_remote_repo=$( get_tag_cabal_remote_repo "${tag}" ) 143 | 144 | cat <<-EOF 145 | remote-repo: ${cabal_remote_repo} 146 | remote-repo-cache: ${HALCYON_BASE}/cabal/remote-repo-cache 147 | avoid-reinstalls: True 148 | reorder-goals: True 149 | require-sandbox: True 150 | jobs: \$ncpus 151 | EOF 152 | } 153 | 154 | 155 | format_cabal_archive_name () { 156 | local tag 157 | expect_args tag -- "$@" 158 | 159 | local cabal_id repo_name cabal_date 160 | cabal_id=$( format_cabal_id "${tag}" ) 161 | repo_name=$( format_cabal_remote_repo_name "${tag}" | tr '[:upper:]' '[:lower:]' ) 162 | cabal_date=$( get_tag_cabal_date "${tag}" ) 163 | 164 | echo "halcyon-cabal-${cabal_id}${repo_name:+-${repo_name}${cabal_date:+-${cabal_date}}}.tar.gz" 165 | } 166 | 167 | 168 | format_base_cabal_archive_name () { 169 | local tag 170 | expect_args tag -- "$@" 171 | 172 | local cabal_id 173 | cabal_id=$( format_cabal_id "${tag}" ) 174 | 175 | echo "halcyon-cabal-${cabal_id}.tar.gz" 176 | } 177 | 178 | 179 | format_updated_cabal_archive_name_prefix () { 180 | local tag 181 | expect_args tag -- "$@" 182 | 183 | local cabal_id repo_name 184 | cabal_id=$( format_cabal_id "${tag}" ) 185 | repo_name=$( format_cabal_remote_repo_name "${tag}" | tr '[:upper:]' '[:lower:]' ) 186 | 187 | echo "halcyon-cabal-${cabal_id}-${repo_name}" 188 | } 189 | 190 | 191 | format_updated_cabal_archive_name_pattern () { 192 | local tag 193 | expect_args tag -- "$@" 194 | 195 | local cabal_id repo_name 196 | cabal_id=$( format_cabal_id "${tag}" ) 197 | repo_name=$( format_cabal_remote_repo_name "${tag}" | tr '[:upper:]' '[:lower:]' ) 198 | 199 | echo "halcyon-cabal-${cabal_id//./\.}-${repo_name//./\.}.*\.tar\.gz" 200 | } 201 | 202 | 203 | format_updated_cabal_archive_name_date () { 204 | local archive_name 205 | expect_args archive_name -- "$@" 206 | 207 | local date_etc 208 | date_etc="${archive_name#halcyon-cabal-*-*-}" 209 | 210 | echo "${date_etc%.tar.gz}" 211 | } 212 | 213 | 214 | hash_cabal_magic () { 215 | local source_dir 216 | expect_args source_dir -- "$@" 217 | 218 | local cabal_magic_hash 219 | if ! cabal_magic_hash=$( hash_tree "${source_dir}/.halcyon" \( -name 'cabal-remote-repo' -or -name 'cabal-version' -prune \) -or -path './cabal*' ); then 220 | log_error 'Failed to hash Cabal magic files' 221 | return 1 222 | fi 223 | 224 | echo "${cabal_magic_hash}" 225 | } 226 | 227 | 228 | copy_cabal_magic () { 229 | expect_vars HALCYON_BASE 230 | 231 | local source_dir 232 | expect_args source_dir -- "$@" 233 | 234 | expect_existing "${HALCYON_BASE}/cabal" || return 1 235 | 236 | local cabal_magic_hash 237 | cabal_magic_hash=$( hash_cabal_magic "${source_dir}" ) || return 1 238 | if [[ -z "${cabal_magic_hash}" ]]; then 239 | return 0 240 | fi 241 | 242 | local file 243 | find_tree "${source_dir}/.halcyon" -type f -path './cabal*' | 244 | while read -r file; do 245 | copy_file "${source_dir}/.halcyon/${file}" \ 246 | "${HALCYON_BASE}/cabal/.halcyon/${file}" || return 1 247 | done || return 0 248 | } 249 | 250 | 251 | build_cabal_dir () { 252 | expect_vars HALCYON_BASE \ 253 | HALCYON_CABAL_NO_STRIP \ 254 | HALCYON_INTERNAL_PLATFORM 255 | 256 | local tag source_dir 257 | expect_args tag source_dir -- "$@" 258 | 259 | if ! rm -rf "${HALCYON_BASE}/cabal"; then 260 | log_error 'Failed to prepare Cabal directory' 261 | return 1 262 | fi 263 | 264 | local ghc_version ghc_major ghc_minor 265 | ghc_version=$( get_tag_ghc_version "${tag}" ) 266 | ghc_major="${ghc_version%%.*}" 267 | ghc_minor="${ghc_version#*.}" 268 | ghc_minor="${ghc_minor%%.*}" 269 | 270 | local cabal_version 271 | cabal_version=$( get_tag_cabal_version "${tag}" ) 272 | 273 | # NOTE: Bootstrapping cabal-install 1.20.* with GHC 7.6.* fails. 274 | if (( ghc_major < 7 || ghc_minor < 8 )); then 275 | log_error "Unsupported GHC and Cabal version combination: ${ghc_version} and ${cabal_version}" 276 | log_error "To use Cabal ${cabal_version}, use GHC 7.8.2 or newer" 277 | return 1 278 | fi 279 | 280 | local cabal_original_url cabal_dir cabal_home_dir 281 | cabal_original_url=$( map_cabal_version_to_original_url "${cabal_version}" ) || return 1 282 | cabal_dir=$( get_tmp_dir "cabal-${cabal_version}" ) || return 1 283 | cabal_home_dir=$( get_tmp_dir 'disregard-this-advice' ) || return 1 284 | 285 | log 'Building Cabal directory' 286 | 287 | acquire_original_source "${cabal_original_url}" "${cabal_dir}" || return 1 288 | 289 | # NOTE: cabal-install 1.22.0.1 is not packaged properly. 290 | if [[ "${cabal_version}" == '1.22.0.1' ]]; then 291 | mv "${cabal_dir}/cabal-cabal-install-v1.22.0.1/cabal-install" "${cabal_dir}/cabal-install-1.22.0.1" || return 1 292 | fi 293 | 294 | local cabal_sub_dir 295 | if ! cabal_sub_dir=$( 296 | find_tree "${cabal_dir}" -type d -maxdepth 1 -name 'cabal-install-*' | 297 | match_exactly_one 298 | ); then 299 | log_error 'Failed to detect Cabal source directory' 300 | return 1 301 | fi 302 | 303 | local cabal_build_dir 304 | cabal_build_dir="${cabal_dir}/${cabal_sub_dir}" 305 | expect_existing "${cabal_build_dir}" || return 1 306 | 307 | if [[ -f "${source_dir}/.halcyon/cabal-pre-build-hook" ]]; then 308 | log 'Executing Cabal pre-build hook' 309 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 310 | HALCYON_GHC_VERSION="${ghc_version}" \ 311 | HALCYON_CABAL_VERSION="${cabal_version}" \ 312 | "${source_dir}/.halcyon/cabal-pre-build-hook" \ 313 | "${tag}" "${source_dir}" "${cabal_build_dir}" 2>&1 | quote 314 | then 315 | log_error 'Failed to execute Cabal pre-build hook' 316 | return 1 317 | fi 318 | log 'Cabal pre-build hook executed' 319 | fi 320 | 321 | log 'Bootstrapping Cabal' 322 | 323 | if ! ( 324 | cd "${cabal_build_dir}" && 325 | patch -s <<-EOF 326 | --- a/bootstrap.sh 327 | +++ b/bootstrap.sh 328 | @@ -217,3 +217,3 @@ install_pkg () { 329 | 330 | - \${GHC} --make Setup -o Setup || 331 | + \${GHC} -L"${HALCYON_BASE}/ghc/usr/lib" --make Setup -o Setup || 332 | die "Compiling the Setup script failed." 333 | EOF 334 | ); then 335 | log_error 'Failed to patch Cabal' 336 | return 1 337 | fi 338 | 339 | # NOTE: Bootstrapping cabal-install with GHC 7.8.* may fail unless 340 | # --no-doc is specified. 341 | # https://ghc.haskell.org/trac/ghc/ticket/9174 342 | local bootstrapped_size 343 | if ! ( 344 | cd "${cabal_build_dir}" && 345 | HOME="${cabal_home_dir}" \ 346 | EXTRA_CONFIGURE_OPTS="--extra-lib-dirs=${HALCYON_BASE}/ghc/usr/lib" \ 347 | ./bootstrap.sh --no-doc 2>&1 | quote 348 | ) || 349 | ! copy_file "${cabal_home_dir}/.cabal/bin/cabal" "${HALCYON_BASE}/cabal/bin/cabal" || 350 | ! copy_cabal_magic "${source_dir}" || 351 | ! bootstrapped_size=$( get_size "${HALCYON_BASE}/cabal" ) 352 | then 353 | log_error 'Failed to bootstrap Cabal' 354 | return 1 355 | fi 356 | log "Cabal bootstrapped, ${bootstrapped_size}" 357 | 358 | if [[ -f "${source_dir}/.halcyon/cabal-post-build-hook" ]]; then 359 | log 'Executing Cabal post-build hook' 360 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 361 | HALCYON_GHC_VERSION="${ghc_version}" \ 362 | HALCYON_CABAL_VERSION="${cabal_version}" \ 363 | "${source_dir}/.halcyon/cabal-post-build-hook" \ 364 | "${tag}" "${source_dir}" "${cabal_build_dir}" 2>&1 | quote 365 | then 366 | log_error 'Failed to execute Cabal post-build hook' 367 | return 1 368 | fi 369 | log 'Cabal post-build hook executed' 370 | fi 371 | 372 | if ! (( HALCYON_CABAL_NO_STRIP )); then 373 | log_indent_begin 'Stripping Cabal directory...' 374 | 375 | local stripped_size 376 | if ! strip_tree "${HALCYON_BASE}/cabal" || 377 | ! stripped_size=$( get_size "${HALCYON_BASE}/cabal" ) 378 | then 379 | log_indent_end 'error' 380 | return 1 381 | fi 382 | log_indent_end "done, ${stripped_size}" 383 | fi 384 | 385 | if ! derive_base_cabal_tag "${tag}" >"${HALCYON_BASE}/cabal/.halcyon-tag"; then 386 | log_error 'Failed to write Cabal tag' 387 | return 1 388 | fi 389 | } 390 | 391 | 392 | cabal_update () { 393 | expect_vars HALCYON_BASE 394 | 395 | local stderr 396 | stderr=$( get_tmp_file 'cabal-update.stderr' ) || return 1 397 | 398 | # NOTE: cabal-install 1.20.0.5 enforces the require-sandbox option 399 | # even for the update command. 400 | # https://github.com/haskell/cabal/issues/2309 401 | local updated_size 402 | if ! cabal_do '.' --no-require-sandbox update >"${stderr}" 2>&1 || 403 | ! updated_size=$( get_size "${HALCYON_BASE}/cabal" ) 404 | then 405 | log_indent_end 'error' 406 | quote <"${stderr}" 407 | return 1 408 | fi 409 | log_indent_end "done, ${updated_size}" 410 | } 411 | 412 | 413 | update_cabal_package_db () { 414 | expect_vars HALCYON_BASE 415 | 416 | local tag 417 | expect_args tag -- "$@" 418 | 419 | local ghc_version cabal_version 420 | ghc_version=$( get_tag_ghc_version "${tag}" ) 421 | cabal_version=$( get_tag_cabal_version "${tag}" ) 422 | 423 | log 'Updating Cabal directory' 424 | 425 | if ! format_cabal_config "${tag}" >"${HALCYON_BASE}/cabal/config"; then 426 | log_error 'Failed to write Cabal config' 427 | return 1 428 | fi 429 | 430 | if [[ -f "${source_dir}/.halcyon/cabal-pre-update-hook" ]]; then 431 | log 'Executing Cabal pre-update hook' 432 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 433 | HALCYON_GHC_VERSION="${ghc_version}" \ 434 | HALCYON_CABAL_VERSION="${cabal_version}" \ 435 | "${source_dir}/.halcyon/cabal-pre-update-hook" 2>&1 | quote 436 | then 437 | log_error 'Failed to execute Cabal pre-update hook' 438 | return 1 439 | fi 440 | log 'Cabal pre-update hook executed' 441 | fi 442 | 443 | log_indent_begin 'Updating Cabal package database...' 444 | 445 | cabal_update || return 1 446 | 447 | if [[ -f "${source_dir}/.halcyon/cabal-post-update-hook" ]]; then 448 | log 'Executing Cabal post-update hook' 449 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 450 | HALCYON_GHC_VERSION="${ghc_version}" \ 451 | HALCYON_CABAL_VERSION="${cabal_version}" \ 452 | "${source_dir}/.halcyon/cabal-post-update-hook" 2>&1 | quote 453 | then 454 | log_error 'Failed to execute Cabal post-update hook' 455 | return 1 456 | fi 457 | log 'Cabal post-update hook executed' 458 | fi 459 | 460 | if ! derive_updated_cabal_tag "${tag}" >"${HALCYON_BASE}/cabal/.halcyon-tag"; then 461 | log_error 'Failed to write Cabal tag' 462 | return 1 463 | fi 464 | } 465 | 466 | 467 | archive_cabal_dir () { 468 | expect_vars HALCYON_BASE HALCYON_NO_ARCHIVE \ 469 | HALCYON_INTERNAL_PLATFORM 470 | 471 | if (( HALCYON_NO_ARCHIVE )); then 472 | return 0 473 | fi 474 | 475 | expect_existing "${HALCYON_BASE}/cabal/.halcyon-tag" || return 1 476 | 477 | local cabal_tag archive_name 478 | cabal_tag=$( detect_cabal_tag "${HALCYON_BASE}/cabal/.halcyon-tag" ) || return 1 479 | archive_name=$( format_cabal_archive_name "${cabal_tag}" ) 480 | 481 | log 'Archiving Cabal directory' 482 | 483 | create_cached_archive "${HALCYON_BASE}/cabal" "${archive_name}" || return 1 484 | upload_cached_file "${HALCYON_INTERNAL_PLATFORM}" "${archive_name}" || return 1 485 | 486 | local cabal_date 487 | cabal_date=$( get_tag_cabal_date "${cabal_tag}" ) 488 | if [[ -z "${cabal_date}" ]]; then 489 | return 0 490 | fi 491 | 492 | local updated_prefix updated_pattern 493 | updated_prefix=$( format_updated_cabal_archive_name_prefix "${cabal_tag}" ) 494 | updated_pattern=$( format_updated_cabal_archive_name_pattern "${cabal_tag}" ) 495 | 496 | delete_matching_private_stored_files "${HALCYON_INTERNAL_PLATFORM}" "${updated_prefix}" "${updated_pattern}" "${archive_name}" || return 1 497 | } 498 | 499 | 500 | validate_base_cabal_dir () { 501 | expect_vars HALCYON_BASE 502 | 503 | local tag 504 | expect_args tag -- "$@" 505 | 506 | local base_tag 507 | base_tag=$( derive_base_cabal_tag "${tag}" ) 508 | detect_tag "${HALCYON_BASE}/cabal/.halcyon-tag" "${base_tag//./\.}" || return 1 509 | } 510 | 511 | 512 | validate_updated_cabal_date () { 513 | local candidate_date 514 | expect_args candidate_date -- "$@" 515 | 516 | local today_date 517 | today_date=$( get_date '+%Y-%m-%d' ) 518 | 519 | if [[ "${candidate_date}" < "${today_date}" ]]; then 520 | return 1 521 | fi 522 | } 523 | 524 | 525 | validate_updated_cabal_dir () { 526 | expect_vars HALCYON_BASE 527 | 528 | local tag 529 | expect_args tag -- "$@" 530 | 531 | local updated_pattern candidate_tag 532 | updated_pattern=$( derive_updated_cabal_tag_pattern "${tag}" ) 533 | candidate_tag=$( detect_tag "${HALCYON_BASE}/cabal/.halcyon-tag" "${updated_pattern}" ) || return 1 534 | 535 | local candidate_remote_repo candidate_date 536 | candidate_remote_repo=$( get_tag_cabal_remote_repo "${candidate_tag}" ) 537 | if ! is_stackage "${candidate_remote_repo}"; then 538 | candidate_date=$( get_tag_cabal_date "${candidate_tag}" ) 539 | validate_updated_cabal_date "${candidate_date}" || return 1 540 | fi 541 | 542 | if [[ ! -f "${HALCYON_BASE}/cabal/config" ]]; then 543 | return 1 544 | fi 545 | 546 | echo "${candidate_tag}" 547 | } 548 | 549 | 550 | match_updated_cabal_archive_name () { 551 | local tag 552 | expect_args tag -- "$@" 553 | 554 | local updated_pattern candidate_name 555 | updated_pattern=$( format_updated_cabal_archive_name_pattern "${tag}" ) 556 | candidate_name=$( 557 | filter_matching "^${updated_pattern}$" | 558 | sort_natural -u | 559 | filter_last | 560 | match_exactly_one 561 | ) || return 1 562 | 563 | local cabal_remote_repo candidate_date 564 | cabal_remote_repo=$( get_tag_cabal_remote_repo "${tag}" ) 565 | if ! is_stackage "${cabal_remote_repo}"; then 566 | candidate_date=$( format_updated_cabal_archive_name_date "${candidate_name}" ) 567 | validate_updated_cabal_date "${candidate_date}" || return 1 568 | fi 569 | 570 | echo "${candidate_name}" 571 | } 572 | 573 | 574 | restore_base_cabal_dir () { 575 | expect_vars HALCYON_BASE \ 576 | HALCYON_INTERNAL_PLATFORM 577 | 578 | local tag 579 | expect_args tag -- "$@" 580 | 581 | local base_name 582 | base_name=$( format_base_cabal_archive_name "${tag}" ) 583 | 584 | if validate_base_cabal_dir "${tag}" >'/dev/null'; then 585 | log 'Using existing Cabal directory' 586 | 587 | touch_cached_file "${base_name}" 588 | return 0 589 | fi 590 | rm -rf "${HALCYON_BASE}/cabal" || true 591 | 592 | log 'Restoring Cabal directory' 593 | 594 | if ! extract_cached_archive_over "${base_name}" "${HALCYON_BASE}/cabal" || 595 | ! validate_base_cabal_dir "${tag}" >'/dev/null' 596 | then 597 | rm -rf "${HALCYON_BASE}/cabal" || true 598 | cache_stored_file "${HALCYON_INTERNAL_PLATFORM}" "${base_name}" || return 1 599 | 600 | if ! extract_cached_archive_over "${base_name}" "${HALCYON_BASE}/cabal" || 601 | ! validate_base_cabal_dir "${tag}" >'/dev/null' 602 | then 603 | rm -rf "${HALCYON_BASE}/cabal" || true 604 | 605 | log_warning 'Failed to restore Cabal directory' 606 | return 1 607 | fi 608 | else 609 | touch_cached_file "${base_name}" 610 | fi 611 | } 612 | 613 | 614 | restore_cached_updated_cabal_dir () { 615 | expect_vars HALCYON_BASE HALCYON_CACHE 616 | 617 | local tag 618 | expect_args tag -- "$@" 619 | 620 | local updated_name 621 | updated_name=$( 622 | find_tree "${HALCYON_CACHE}" -maxdepth 1 -type f | 623 | match_updated_cabal_archive_name "${tag}" 624 | ) || true 625 | 626 | if validate_updated_cabal_dir "${tag}" >'/dev/null'; then 627 | log 'Using existing Cabal directory' 628 | 629 | touch_cached_file "${updated_name}" 630 | return 0 631 | fi 632 | rm -rf "${HALCYON_BASE}/cabal" || true 633 | 634 | if [[ -z "${updated_name}" ]]; then 635 | return 1 636 | fi 637 | 638 | log 'Restoring Cabal directory' 639 | 640 | if ! extract_cached_archive_over "${updated_name}" "${HALCYON_BASE}/cabal" || 641 | ! validate_updated_cabal_dir "${tag}" >'/dev/null' 642 | then 643 | rm -rf "${HALCYON_BASE}/cabal" || true 644 | 645 | log_warning 'Failed to restore Cabal directory' 646 | return 1 647 | else 648 | touch_cached_file "${updated_name}" 649 | fi 650 | } 651 | 652 | 653 | restore_updated_cabal_dir () { 654 | expect_vars HALCYON_BASE \ 655 | HALCYON_INTERNAL_PLATFORM 656 | 657 | local tag 658 | expect_args tag -- "$@" 659 | 660 | local archive_prefix 661 | archive_prefix=$( format_updated_cabal_archive_name_prefix "${tag}" ) 662 | 663 | if restore_cached_updated_cabal_dir "${tag}"; then 664 | return 0 665 | fi 666 | 667 | log 'Locating Cabal directories' 668 | 669 | local updated_name 670 | updated_name=$( 671 | list_stored_files "${HALCYON_INTERNAL_PLATFORM}/${archive_prefix}" | 672 | sed "s:^${HALCYON_INTERNAL_PLATFORM}/::" | 673 | match_updated_cabal_archive_name "${tag}" 674 | ) || return 1 675 | 676 | log 'Restoring Cabal directory' 677 | 678 | cache_stored_file "${HALCYON_INTERNAL_PLATFORM}" "${updated_name}" || return 1 679 | 680 | if ! extract_cached_archive_over "${updated_name}" "${HALCYON_BASE}/cabal" || 681 | ! validate_updated_cabal_dir "${tag}" >'/dev/null' 682 | then 683 | rm -rf "${HALCYON_BASE}/cabal" || true 684 | 685 | log_warning 'Failed to restore Cabal directory' 686 | return 1 687 | fi 688 | } 689 | 690 | 691 | install_cabal_dir () { 692 | expect_vars HALCYON_NO_BUILD HALCYON_NO_BUILD_DEPENDENCIES \ 693 | HALCYON_CABAL_REBUILD HALCYON_CABAL_UPDATE HALCYON_CABAL_NO_UPDATE 694 | 695 | local tag source_dir 696 | expect_args tag source_dir -- "$@" 697 | 698 | if ! (( HALCYON_CABAL_REBUILD )); then 699 | if ! (( HALCYON_CABAL_NO_UPDATE )); then 700 | if ! (( HALCYON_CABAL_UPDATE )) && 701 | restore_updated_cabal_dir "${tag}" 702 | then 703 | return 0 704 | fi 705 | fi 706 | 707 | if restore_base_cabal_dir "${tag}"; then 708 | if ! (( HALCYON_CABAL_NO_UPDATE )); then 709 | update_cabal_package_db "${tag}" || return 1 710 | archive_cabal_dir || return 1 711 | fi 712 | return 0 713 | fi 714 | 715 | # NOTE: Returns 2 if build is needed. 716 | if (( HALCYON_NO_BUILD )) || (( HALCYON_NO_BUILD_DEPENDENCIES )); then 717 | log_error 'Cannot build Cabal directory' 718 | return 2 719 | fi 720 | fi 721 | 722 | build_cabal_dir "${tag}" "${source_dir}" || return 1 723 | archive_cabal_dir || return 1 724 | if ! (( HALCYON_CABAL_NO_UPDATE )); then 725 | update_cabal_package_db "${tag}" || return 1 726 | archive_cabal_dir || return 1 727 | fi 728 | } 729 | 730 | 731 | cabal_do () { 732 | expect_vars HALCYON_BASE 733 | 734 | expect_existing "${HALCYON_BASE}/cabal/.halcyon-tag" || return 1 735 | 736 | local work_dir 737 | expect_args work_dir -- "$@" 738 | shift 739 | 740 | expect_existing "${work_dir}" || return 1 741 | ( 742 | cd "${work_dir}" && 743 | cabal --config-file="${HALCYON_BASE}/cabal/config" "$@" 744 | ) || return 1 745 | } 746 | 747 | 748 | sandboxed_cabal_do () { 749 | expect_vars HALCYON_BASE 750 | 751 | local work_dir 752 | expect_args work_dir -- "$@" 753 | shift 754 | 755 | expect_existing "${HALCYON_BASE}/sandbox" "${work_dir}" || return 1 756 | 757 | # NOTE: Specifying a cabal.sandbox.config file changes where Cabal 758 | # looks for a cabal.config file. 759 | # https://github.com/haskell/cabal/issues/1915 760 | local saved_config 761 | saved_config='' 762 | if [[ -f "${HALCYON_BASE}/sandbox/cabal.config" ]]; then 763 | if ! saved_config=$( get_tmp_file 'saved-sandbox-cabal.config' ) || 764 | ! mv "${HALCYON_BASE}/sandbox/cabal.config" "${saved_config}" 765 | then 766 | log_error 'Failed to put away existing sandbox Cabal config' 767 | return 1 768 | fi 769 | fi 770 | if [[ -f "${work_dir}/cabal.config" ]]; then 771 | if ! copy_file "${work_dir}/cabal.config" "${HALCYON_BASE}/sandbox/cabal.config"; then 772 | log_error 'Failed to copy temporary sandbox Cabal config' 773 | return 1 774 | fi 775 | fi 776 | 777 | local status 778 | status=0 779 | if ! cabal_do "${work_dir}" --sandbox-config-file="${HALCYON_BASE}/sandbox/cabal.sandbox.config" "$@"; then 780 | status=1 781 | fi 782 | 783 | if ! rm -f "${HALCYON_BASE}/sandbox/cabal.config"; then 784 | log_error 'Failed to remove temporary sandbox Cabal config' 785 | return 1 786 | fi 787 | if [[ -n "${saved_config}" ]]; then 788 | if ! mv "${saved_config}" "${HALCYON_BASE}/sandbox/cabal.config"; then 789 | log_error 'Failed to put back existing sandbox Cabal config' 790 | return 1 791 | fi 792 | fi 793 | 794 | return "${status}" 795 | } 796 | 797 | 798 | cabal_create_sandbox () { 799 | expect_vars HALCYON_BASE 800 | 801 | local stderr 802 | stderr=$( get_tmp_file 'cabal-sandbox-init.stderr' ) || return 1 803 | 804 | mkdir -p "${HALCYON_BASE}/sandbox" || return 1 805 | 806 | if ! cabal_do "${HALCYON_BASE}/sandbox" sandbox init --sandbox '.' >"${stderr}" 2>&1; then 807 | quote <"${stderr}" 808 | return 1 809 | fi 810 | } 811 | 812 | 813 | cabal_dry_freeze_constraints () { 814 | local label source_dir 815 | expect_args label source_dir -- "$@" 816 | 817 | local stderr 818 | stderr=$( get_tmp_file 'cabal-freeze.stderr' ) || return 1 819 | 820 | local -a opts_a 821 | opts_a=() 822 | opts_a+=( --dry-run ) 823 | if [[ -f "${source_dir}/.halcyon/sandbox-extra-configure-flags" ]]; then 824 | while read -r flag; do 825 | case "${flag}" in 826 | '--enable-benchmarks'|'--disable-benchmarks'|'--enable-tests'|'--disable-tests') 827 | opts_a+=( "${flag}" ) 828 | esac 829 | done <"${source_dir}/.halcyon/sandbox-extra-configure-flags" || true 830 | fi 831 | 832 | # NOTE: Cabal automatically sets global installed constraints for 833 | # installed packages, even during a freeze dry run. Hence, if a 834 | # local constraint conflicts with an installed package, Cabal will 835 | # fail to resolve dependencies. 836 | # Cabal freeze always ignores any constraints set both in the local 837 | # cabal.config file, and in the global ~/.cabal/config file. 838 | # https://github.com/haskell/cabal/issues/2178 839 | # https://github.com/haskell/cabal/issues/2265 840 | local constraints 841 | if ! constraints=$( 842 | cabal_do "${source_dir}" --no-require-sandbox freeze "${opts_a[@]}" 2>"${stderr}" | 843 | read_constraints_from_cabal_dry_freeze | 844 | filter_correct_constraints "${label}" | 845 | sort_natural 846 | ); then 847 | quote <"${stderr}" 848 | return 1 849 | fi 850 | 851 | echo "${constraints}" 852 | } 853 | 854 | 855 | sandboxed_cabal_dry_freeze_constraints () { 856 | local label source_dir 857 | expect_args label source_dir -- "$@" 858 | 859 | local stderr 860 | stderr=$( get_tmp_file 'cabal-freeze.stderr' ) || return 1 861 | 862 | local -a opts_a 863 | opts_a=() 864 | opts_a+=( --dry-run ) 865 | if [[ -f "${source_dir}/.halcyon/sandbox-extra-configure-flags" ]]; then 866 | while read -r flag; do 867 | case "${flag}" in 868 | '--enable-benchmarks'|'--disable-benchmarks'|'--enable-tests'|'--disable-tests') 869 | opts_a+=( "${flag}" ) 870 | esac 871 | done <"${source_dir}/.halcyon/sandbox-extra-configure-flags" || true 872 | fi 873 | 874 | local constraints 875 | if ! constraints=$( 876 | sandboxed_cabal_do "${source_dir}" freeze "${opts_a[@]}" 2>"${stderr}" | 877 | read_constraints_from_cabal_dry_freeze | 878 | filter_correct_constraints "${label}" | 879 | sort_natural 880 | ); then 881 | quote <"${stderr}" 882 | return 1 883 | fi 884 | 885 | echo "${constraints}" 886 | } 887 | 888 | 889 | temporarily_sandboxed_cabal_dry_freeze_constraints () { 890 | expect_vars HALCYON_BASE 891 | 892 | local label source_dir 893 | expect_args label source_dir -- "$@" 894 | 895 | local saved_sandbox 896 | saved_sandbox='' 897 | if [[ -d "${HALCYON_BASE}/sandbox" ]]; then 898 | if ! saved_sandbox=$( get_tmp_dir 'saved-sandbox' ) || 899 | ! mv "${HALCYON_BASE}/sandbox" "${saved_sandbox}" 900 | then 901 | log_error 'Failed to put away existing sandbox' 902 | return 1 903 | fi 904 | fi 905 | 906 | log 'Creating temporary sandbox' 907 | 908 | if ! cabal_create_sandbox; then 909 | log_error 'Failed to create temporary sandbox' 910 | return 1 911 | fi 912 | 913 | if ! add_sandbox_sources "${source_dir}"; then 914 | log_error 'Failed to add temporary sandbox sources' 915 | return 1 916 | fi 917 | 918 | local constraints 919 | constraints=$( sandboxed_cabal_dry_freeze_constraints "${label}" "${source_dir}" ) || return 1 920 | 921 | if ! rm -rf "${HALCYON_BASE}/sandbox"; then 922 | log_error 'Failed to remove temporary sandbox' 923 | return 1 924 | fi 925 | if [[ -n "${saved_sandbox}" ]]; then 926 | if ! mv "${saved_sandbox}" "${HALCYON_BASE}/sandbox"; then 927 | log_error 'Failed to put back existing sandbox' 928 | return 1 929 | fi 930 | fi 931 | 932 | echo "${constraints}" 933 | } 934 | 935 | 936 | cabal_determine_constraints () { 937 | local label source_dir 938 | expect_args label source_dir -- "$@" 939 | 940 | local constraints 941 | if [[ -f "${source_dir}/.halcyon/sandbox-sources" ]]; then 942 | constraints=$( temporarily_sandboxed_cabal_dry_freeze_constraints "${label}" "${source_dir}" ) || return 1 943 | else 944 | constraints=$( cabal_dry_freeze_constraints "${label}" "${source_dir}" ) || return 1 945 | fi 946 | 947 | echo "${constraints}" 948 | } 949 | 950 | 951 | cabal_unpack_over () { 952 | local thing unpack_dir 953 | expect_args thing unpack_dir -- "$@" 954 | 955 | local stderr 956 | stderr=$( get_tmp_file 'cabal-unpack.stderr' ) || return 1 957 | 958 | rm -rf "${unpack_dir}" || return 1 959 | mkdir -p "${unpack_dir}" || return 1 960 | 961 | local label 962 | if ! label=$( 963 | cabal_do "${unpack_dir}" unpack "${thing}" 2>"${stderr}" | 964 | filter_matching '^Unpacking to ' | 965 | match_exactly_one | 966 | sed 's:^Unpacking to \(.*\)/$:\1:' 967 | ); then 968 | quote <"${stderr}" 969 | return 1 970 | fi 971 | 972 | echo "${label}" 973 | } 974 | 975 | 976 | populate_cabal_setup_exe_cache () { 977 | expect_vars HOME 978 | 979 | # NOTE: Haste needs Cabal to generate HOME/.cabal/setup-exe-cache. 980 | # https://github.com/valderman/haste-compiler/issues/257 981 | if [[ -f "${HOME}/.cabal/setup-exe-cache" ]]; then 982 | return 0 983 | fi 984 | 985 | log 'Populating Cabal setup executable cache' 986 | 987 | local setup_dir 988 | setup_dir=$( get_tmp_dir 'cabal-setup-exe-cache-sandbox' ) || return 1 989 | 990 | if ! mkdir -p "${setup_dir}" || 991 | ! cabal_do "${setup_dir}" sandbox init --sandbox '.' 2>&1 | quote || 992 | ! cabal_do "${setup_dir}" install 'populate-setup-exe-cache' 2>&1 | quote 993 | then 994 | log_error 'Failed to populate Cabal setup executable cache' 995 | return 1 996 | fi 997 | expect_existing "${HOME}/.cabal/setup-exe-cache" || return 1 998 | } 999 | -------------------------------------------------------------------------------- /src/ghc.sh: -------------------------------------------------------------------------------- 1 | map_ghc_version_to_source_url () { 2 | local ghc_version 3 | expect_args ghc_version -- "$@" 4 | 5 | case "${ghc_version}" in 6 | '6.10.4') echo 'https://downloads.haskell.org/~ghc/6.10.4/ghc-6.10.4-src.tar.bz2';; 7 | '6.12.3') echo 'https://downloads.haskell.org/~ghc/6.12.3/ghc-6.12.3-src.tar.bz2';; 8 | '7.0.4') echo 'https://downloads.haskell.org/~ghc/7.0.4/ghc-7.0.4-src.tar.bz2';; 9 | '7.2.2') echo 'https://downloads.haskell.org/~ghc/7.2.2/ghc-7.2.2-src.tar.bz2';; 10 | '7.4.2') echo 'https://downloads.haskell.org/~ghc/7.4.2/ghc-7.4.2-src.tar.bz2';; 11 | '7.6.1') echo 'https://downloads.haskell.org/~ghc/7.6.1/ghc-7.6.1-src.tar.bz2';; 12 | '7.6.3') echo 'https://downloads.haskell.org/~ghc/7.6.3/ghc-7.6.3-src.tar.bz2';; 13 | '7.8.2') echo 'https://downloads.haskell.org/~ghc/7.8.2/ghc-7.8.2-src.tar.xz';; 14 | '7.8.3') echo 'https://downloads.haskell.org/~ghc/7.8.3/ghc-7.8.3-src.tar.xz';; 15 | '7.8.4') echo 'https://downloads.haskell.org/~ghc/7.8.4/ghc-7.8.4-src.tar.xz';; 16 | '7.10.1') echo 'https://downloads.haskell.org/~ghc/7.10.1/ghc-7.10.1-src.tar.xz';; 17 | '7.10.2-rc1') echo 'https://downloads.haskell.org/~ghc/7.10.2-rc1/ghc-7.10.1.20150612-src.tar.xz';; 18 | *) 19 | log_error "Unsupported GHC version: ${ghc_version}" 20 | return 1 21 | esac 22 | } 23 | 24 | 25 | map_ghc_version_to_freebsd_i386_url () { 26 | local ghc_version 27 | expect_args ghc_version -- "$@" 28 | 29 | case "${ghc_version}" in 30 | '6.12.3') echo 'https://downloads.haskell.org/~ghc/6.12.3/ghc-6.12.3-i386-freebsd-8.tar.bz2';; 31 | '7.0.4') echo 'https://downloads.haskell.org/~ghc/7.0.4/ghc-7.0.4-i386-unknown-freebsd.tar.bz2';; 32 | '7.2.2') echo 'https://downloads.haskell.org/~ghc/7.2.2/ghc-7.2.2-i386-unknown-freebsd.tar.bz2';; 33 | '7.4.2') echo 'https://downloads.haskell.org/~ghc/7.4.2/ghc-7.4.2-i386-unknown-freebsd.tar.bz2';; 34 | '7.6.1') echo 'https://downloads.haskell.org/~ghc/7.6.1/ghc-7.6.1-i386-unknown-freebsd.tar.bz2';; 35 | '7.6.3') echo 'https://downloads.haskell.org/~ghc/7.6.3/ghc-7.6.3-i386-unknown-freebsd.tar.bz2';; 36 | '7.8.2') echo 'https://downloads.haskell.org/~ghc/7.8.2/ghc-7.8.2-i386-portbld-freebsd.tar.xz';; 37 | '7.8.3') echo 'https://downloads.haskell.org/~ghc/7.8.3/ghc-7.8.3-i386-portbld-freebsd.tar.xz';; 38 | '7.8.4') echo 'https://downloads.haskell.org/~ghc/7.8.4/ghc-7.8.4-i386-portbld-freebsd.tar.xz';; 39 | '7.10.1') echo 'https://downloads.haskell.org/~ghc/7.10.1/ghc-7.10.1-i386-portbld-freebsd.tar.xz';; 40 | *) 41 | log_error "Unsupported GHC version for FreeBSD (i386): ${ghc_version}" 42 | return 1 43 | esac 44 | } 45 | 46 | 47 | map_ghc_version_to_freebsd_x86_64_url () { 48 | local ghc_version 49 | expect_args ghc_version -- "$@" 50 | 51 | case "${ghc_version}" in 52 | '6.12.3') echo 'https://downloads.haskell.org/~ghc/6.12.3/ghc-6.12.3-amd64-freebsd-8.tar.bz2';; 53 | '7.0.4') echo 'https://downloads.haskell.org/~ghc/7.0.4/ghc-7.0.4-x86_64-unknown-freebsd.tar.bz2';; 54 | '7.2.2') echo 'https://downloads.haskell.org/~ghc/7.2.2/ghc-7.2.2-x86_64-unknown-freebsd.tar.bz2';; 55 | '7.4.2') echo 'https://downloads.haskell.org/~ghc/7.4.2/ghc-7.4.2-x86_64-unknown-freebsd.tar.bz2';; 56 | '7.6.1') echo 'https://downloads.haskell.org/~ghc/7.6.1/ghc-7.6.1-x86_64-unknown-freebsd.tar.bz2';; 57 | '7.6.3') echo 'https://downloads.haskell.org/~ghc/7.6.3/ghc-7.6.3-x86_64-unknown-freebsd.tar.bz2';; 58 | '7.8.2') echo 'https://downloads.haskell.org/~ghc/7.8.2/ghc-7.8.2-x86_64-portbld-freebsd.tar.xz';; 59 | '7.8.3') echo 'https://downloads.haskell.org/~ghc/7.8.3/ghc-7.8.3-x86_64-portbld-freebsd.tar.xz';; 60 | '7.8.4') echo 'https://downloads.haskell.org/~ghc/7.8.4/ghc-7.8.4-x86_64-portbld-freebsd.tar.xz';; 61 | '7.10.1') echo 'https://downloads.haskell.org/~ghc/7.10.1/ghc-7.10.1-x86_64-portbld-freebsd.tar.xz';; 62 | *) 63 | log_error "Unsupported GHC version for FreeBSD (x86_64): ${ghc_version}" 64 | return 1 65 | esac 66 | } 67 | 68 | 69 | map_ghc_version_to_gmp5_linux_i386_url () { 70 | local ghc_version 71 | expect_args ghc_version -- "$@" 72 | 73 | case "${ghc_version}" in 74 | '7.8.2') echo 'https://downloads.haskell.org/~ghc/7.8.2/ghc-7.8.2-i386-unknown-linux-deb7.tar.xz';; 75 | '7.8.3') echo 'https://downloads.haskell.org/~ghc/7.8.3/ghc-7.8.3-i386-unknown-linux-deb7.tar.xz';; 76 | '7.8.4') echo 'https://downloads.haskell.org/~ghc/7.8.4/ghc-7.8.4-i386-unknown-linux-deb7.tar.xz';; 77 | '7.10.1') echo 'https://downloads.haskell.org/~ghc/7.10.1/ghc-7.10.1-i386-unknown-linux-deb7.tar.xz';; 78 | '7.10.2-rc1') echo 'https://downloads.haskell.org/~ghc/7.10.2-rc1/ghc-7.10.1.20150612-i386-unknown-linux-deb7.tar.xz';; 79 | *) 80 | log_error "Unsupported GHC version for Linux (i386): ${ghc_version} (GMP 5)" 81 | return 1 82 | esac 83 | } 84 | 85 | 86 | map_ghc_version_to_gmp4_linux_i386_url () { 87 | local ghc_version 88 | expect_args ghc_version -- "$@" 89 | 90 | case "${ghc_version}" in 91 | '6.10.4') echo 'https://downloads.haskell.org/~ghc/6.10.4/ghc-6.10.4-i386-unknown-linux-n.tar.bz2';; 92 | '6.12.3') echo 'https://downloads.haskell.org/~ghc/6.12.3/ghc-6.12.3-i386-unknown-linux-n.tar.bz2';; 93 | '7.0.4') echo 'https://downloads.haskell.org/~ghc/7.0.4/ghc-7.0.4-i386-unknown-linux.tar.bz2';; 94 | '7.2.2') echo 'https://downloads.haskell.org/~ghc/7.2.2/ghc-7.2.2-i386-unknown-linux.tar.bz2';; 95 | '7.4.2') echo 'https://downloads.haskell.org/~ghc/7.4.2/ghc-7.4.2-i386-unknown-linux.tar.bz2';; 96 | '7.6.1') echo 'https://downloads.haskell.org/~ghc/7.6.1/ghc-7.6.1-i386-unknown-linux.tar.bz2';; 97 | '7.6.3') echo 'https://downloads.haskell.org/~ghc/7.6.3/ghc-7.6.3-i386-unknown-linux.tar.bz2';; 98 | '7.8.2') echo 'https://downloads.haskell.org/~ghc/7.8.2/ghc-7.8.2-i386-unknown-linux-centos65.tar.xz';; 99 | '7.8.3') echo 'https://downloads.haskell.org/~ghc/7.8.3/ghc-7.8.3-i386-unknown-linux-centos65.tar.xz';; 100 | '7.8.4') echo 'https://downloads.haskell.org/~ghc/7.8.4/ghc-7.8.4-i386-unknown-linux-centos65.tar.xz';; 101 | *) 102 | log_error "Unsupported GHC version for Linux (i386): ${ghc_version} (GMP 4)" 103 | return 1 104 | esac 105 | } 106 | 107 | 108 | map_ghc_version_to_gmp5_linux_x86_64_url () { 109 | local ghc_version 110 | expect_args ghc_version -- "$@" 111 | 112 | case "${ghc_version}" in 113 | '7.8.2') echo 'https://downloads.haskell.org/~ghc/7.8.2/ghc-7.8.2-x86_64-unknown-linux-deb7.tar.xz';; 114 | '7.8.3') echo 'https://downloads.haskell.org/~ghc/7.8.3/ghc-7.8.3-x86_64-unknown-linux-deb7.tar.xz';; 115 | '7.8.4') echo 'https://downloads.haskell.org/~ghc/7.8.4/ghc-7.8.4-x86_64-unknown-linux-deb7.tar.xz';; 116 | '7.10.1') echo 'https://downloads.haskell.org/~ghc/7.10.1/ghc-7.10.1-x86_64-unknown-linux-deb7.tar.xz';; 117 | '7.10.2-rc1') echo 'https://downloads.haskell.org/~ghc/7.10.2-rc1/ghc-7.10.1.20150612-x86_64-unknown-linux-deb7.tar.xz';; 118 | *) 119 | log_error "Unsupported GHC version for Linux (x86_64): ${ghc_version} (GMP 5)" 120 | return 1 121 | esac 122 | } 123 | 124 | 125 | map_ghc_version_to_gmp4_linux_x86_64_url () { 126 | local ghc_version 127 | expect_args ghc_version -- "$@" 128 | 129 | case "${ghc_version}" in 130 | '6.10.4') echo 'https://downloads.haskell.org/~ghc/6.10.4/ghc-6.10.4-x86_64-unknown-linux-n.tar.bz2';; 131 | '6.12.3') echo 'https://downloads.haskell.org/~ghc/6.12.3/ghc-6.12.3-x86_64-unknown-linux-n.tar.bz2';; 132 | '7.0.4') echo 'https://downloads.haskell.org/~ghc/7.0.4/ghc-7.0.4-x86_64-unknown-linux.tar.bz2';; 133 | '7.2.2') echo 'https://downloads.haskell.org/~ghc/7.2.2/ghc-7.2.2-x86_64-unknown-linux.tar.bz2';; 134 | '7.4.2') echo 'https://downloads.haskell.org/~ghc/7.4.2/ghc-7.4.2-x86_64-unknown-linux.tar.bz2';; 135 | '7.6.1') echo 'https://downloads.haskell.org/~ghc/7.6.1/ghc-7.6.1-x86_64-unknown-linux.tar.bz2';; 136 | '7.6.3') echo 'https://downloads.haskell.org/~ghc/7.6.3/ghc-7.6.3-x86_64-unknown-linux.tar.bz2';; 137 | '7.8.2') echo 'https://downloads.haskell.org/~ghc/7.8.2/ghc-7.8.2-x86_64-unknown-linux-centos65.tar.xz';; 138 | '7.8.3') echo 'https://downloads.haskell.org/~ghc/7.8.3/ghc-7.8.3-x86_64-unknown-linux-centos65.tar.xz';; 139 | '7.8.4') echo 'https://downloads.haskell.org/~ghc/7.8.4/ghc-7.8.4-x86_64-unknown-linux-centos65.tar.xz';; 140 | *) 141 | log_error "Unsupported GHC version for Linux (x86_64): ${ghc_version} (GMP 4)" 142 | return 1 143 | esac 144 | } 145 | 146 | 147 | map_ghc_version_to_osx_i386_url () { 148 | local ghc_version 149 | expect_args ghc_version -- "$@" 150 | 151 | case "${ghc_version}" in 152 | '7.0.4') echo 'https://downloads.haskell.org/~ghc/7.0.4/ghc-7.0.4-i386-apple-darwin.tar.bz2';; 153 | '7.2.2') echo 'https://downloads.haskell.org/~ghc/7.2.2/ghc-7.2.2-i386-apple-darwin.tar.bz2';; 154 | '7.4.2') echo 'https://downloads.haskell.org/~ghc/7.4.2/ghc-7.4.2-i386-apple-darwin.tar.bz2';; 155 | '7.6.1') echo 'https://downloads.haskell.org/~ghc/7.6.1/ghc-7.6.1-i386-apple-darwin.tar.bz2';; 156 | '7.6.3') echo 'https://downloads.haskell.org/~ghc/7.6.3/ghc-7.6.3-i386-apple-darwin.tar.bz2';; 157 | *) 158 | log_error "Unsupported GHC version for OS X (i386): ${ghc_version}" 159 | return 1 160 | esac 161 | } 162 | 163 | 164 | map_ghc_version_to_osx_x86_64_url () { 165 | local ghc_version 166 | expect_args ghc_version -- "$@" 167 | 168 | case "${ghc_version}" in 169 | '7.0.4') echo 'https://downloads.haskell.org/~ghc/7.0.4/ghc-7.0.4-x86_64-apple-darwin.tar.bz2';; 170 | '7.2.2') echo 'https://downloads.haskell.org/~ghc/7.2.2/ghc-7.2.2-x86_64-apple-darwin.tar.bz2';; 171 | '7.4.2') echo 'https://downloads.haskell.org/~ghc/7.4.2/ghc-7.4.2-x86_64-apple-darwin.tar.bz2';; 172 | '7.6.1') echo 'https://downloads.haskell.org/~ghc/7.6.1/ghc-7.6.1-x86_64-apple-darwin.tar.bz2';; 173 | '7.6.3') echo 'https://downloads.haskell.org/~ghc/7.6.3/ghc-7.6.3-x86_64-apple-darwin.tar.bz2';; 174 | '7.8.2') echo 'https://downloads.haskell.org/~ghc/7.8.2/ghc-7.8.2-x86_64-apple-darwin-mavericks.tar.xz';; 175 | '7.8.3') echo 'https://downloads.haskell.org/~ghc/7.8.3/ghc-7.8.3-x86_64-apple-darwin.tar.xz';; 176 | '7.8.4') echo 'https://downloads.haskell.org/~ghc/7.8.4/ghc-7.8.4-x86_64-apple-darwin.tar.xz';; 177 | '7.10.1') echo 'https://downloads.haskell.org/~ghc/7.10.1/ghc-7.10.1-x86_64-apple-darwin.tar.xz';; 178 | '7.10.2-rc1') echo 'https://downloads.haskell.org/~ghc/7.10.2-rc1/ghc-7.10.1.20150612-x86_64-apple-darwin.tar.xz';; 179 | *) 180 | log_error "Unsupported GHC version for OS X (x86_64): ${ghc_version}" 181 | return 1 182 | esac 183 | } 184 | 185 | 186 | map_base_package_version_to_ghc_version () { 187 | local base_version 188 | expect_args base_version -- "$@" 189 | 190 | case "${base_version}" in 191 | '4.1.0.0') echo '6.10.4';; 192 | '4.2.0.2') echo '6.12.3';; 193 | '4.3.1.0') echo '7.0.4';; 194 | '4.4.1.0') echo '7.2.2';; 195 | '4.5.1.0') echo '7.4.2';; 196 | '4.6.0.0') echo '7.6.1';; 197 | '4.6.0.1') echo '7.6.3';; 198 | '4.7.0.0') echo '7.8.2';; 199 | '4.7.0.1') echo '7.8.3';; 200 | '4.7.0.2') echo '7.8.4';; 201 | '4.8.0.0') echo '7.10.1';; 202 | '4.8.1.0') echo '7.10.2-rc1';; 203 | *) 204 | log_error "Unsupported base package version: ${base_version}" 205 | return 1 206 | esac 207 | } 208 | 209 | 210 | map_constraints_to_ghc_version () { 211 | local constraints 212 | expect_args constraints -- "$@" 213 | 214 | local base_version 215 | if ! base_version=$( match_package_version 'base' <<<"${constraints}" ); then 216 | log_warning 'Expected base package constraint' 217 | return 0 218 | fi 219 | 220 | map_base_package_version_to_ghc_version "${base_version}" || return 1 221 | } 222 | 223 | 224 | create_ghc_tag () { 225 | local ghc_version ghc_magic_hash 226 | expect_args ghc_version ghc_magic_hash -- "$@" 227 | 228 | create_tag '' '' '' '' '' \ 229 | "${ghc_version}" "${ghc_magic_hash}" \ 230 | '' '' '' '' \ 231 | '' 232 | } 233 | 234 | 235 | detect_ghc_tag () { 236 | local tag_file 237 | expect_args tag_file -- "$@" 238 | 239 | local tag_pattern 240 | tag_pattern=$( create_ghc_tag '.*' '.*' ) 241 | 242 | local tag 243 | if ! tag=$( detect_tag "${tag_file}" "${tag_pattern}" ); then 244 | log_error 'Failed to detect GHC tag' 245 | return 1 246 | fi 247 | 248 | echo "${tag}" 249 | } 250 | 251 | 252 | derive_ghc_tag () { 253 | local tag 254 | expect_args tag -- "$@" 255 | 256 | local ghc_version ghc_magic_hash 257 | ghc_version=$( get_tag_ghc_version "${tag}" ) 258 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 259 | 260 | create_ghc_tag "${ghc_version}" "${ghc_magic_hash}" 261 | } 262 | 263 | 264 | format_ghc_id () { 265 | local tag 266 | expect_args tag -- "$@" 267 | 268 | local ghc_version ghc_magic_hash 269 | ghc_version=$( get_tag_ghc_version "${tag}" ) 270 | ghc_magic_hash=$( get_tag_ghc_magic_hash "${tag}" ) 271 | 272 | echo "${ghc_version}${ghc_magic_hash:+.${ghc_magic_hash:0:7}}" 273 | } 274 | 275 | 276 | format_ghc_archive_name () { 277 | local tag 278 | expect_args tag -- "$@" 279 | 280 | local ghc_id 281 | ghc_id=$( format_ghc_id "${tag}" ) 282 | 283 | echo "halcyon-ghc-${ghc_id}.tar.gz" 284 | } 285 | 286 | 287 | hash_ghc_magic () { 288 | local source_dir 289 | expect_args source_dir -- "$@" 290 | 291 | local ghc_magic_hash 292 | if ! ghc_magic_hash=$( hash_tree "${source_dir}/.halcyon" -path './ghc*' ); then 293 | log_error 'Failed to hash GHC magic files' 294 | return 1 295 | fi 296 | 297 | echo "${ghc_magic_hash}" 298 | } 299 | 300 | 301 | copy_ghc_magic () { 302 | expect_vars HALCYON_BASE 303 | 304 | local source_dir 305 | expect_args source_dir -- "$@" 306 | 307 | expect_existing "${HALCYON_BASE}/ghc" || return 1 308 | 309 | local ghc_magic_hash 310 | ghc_magic_hash=$( hash_ghc_magic "${source_dir}" ) || return 1 311 | if [[ -z "${ghc_magic_hash}" ]]; then 312 | return 0 313 | fi 314 | 315 | local file 316 | find_tree "${source_dir}/.halcyon" -type f -path './ghc*' | 317 | while read -r file; do 318 | copy_file "${source_dir}/.halcyon/${file}" \ 319 | "${HALCYON_BASE}/ghc/.halcyon/${file}" || return 1 320 | done || return 0 321 | } 322 | 323 | 324 | symlink_ghc_libs () { 325 | expect_vars HALCYON_BASE 326 | 327 | local gmp_name gmp_file tinfo_file 328 | expect_args gmp_name gmp_file tinfo_file -- "$@" 329 | 330 | if [[ -n "${gmp_file}" ]]; then 331 | expect_existing "${gmp_file}" || return 1 332 | 333 | if ! mkdir -p "${HALCYON_BASE}/ghc/usr/lib" || 334 | ! ln -fs "${gmp_file}" "${HALCYON_BASE}/ghc/usr/lib/${gmp_name}" || 335 | ! ln -fs "${gmp_file}" "${HALCYON_BASE}/ghc/usr/lib/libgmp.so" 336 | then 337 | log_error 'Failed to symlink GHC libraries (libgmp.so)' 338 | return 1 339 | fi 340 | fi 341 | 342 | if [[ -n "${tinfo_file}" ]]; then 343 | expect_existing "${tinfo_file}" || return 1 344 | 345 | if ! mkdir -p "${HALCYON_BASE}/ghc/usr/lib" || 346 | ! ln -fs "${tinfo_file}" "${HALCYON_BASE}/ghc/usr/lib/libtinfo.so.5" || 347 | ! ln -fs "${tinfo_file}" "${HALCYON_BASE}/ghc/usr/lib/libtinfo.so" 348 | then 349 | log_error 'Failed to symlink GHC libraries (libtinfo.so)' 350 | return 1 351 | fi 352 | fi 353 | } 354 | 355 | 356 | symlink_ghc_i386_libs () { 357 | expect_vars HALCYON_INTERNAL_PLATFORM 358 | 359 | local tag 360 | expect_args tag -- "$@" 361 | 362 | local description 363 | description=$( format_platform_description "${HALCYON_INTERNAL_PLATFORM}" ) 364 | 365 | local ghc_version ghc_major ghc_minor 366 | ghc_version=$( get_tag_ghc_version "${tag}" ) 367 | ghc_major="${ghc_version%%.*}" 368 | ghc_minor="${ghc_version#*.}" 369 | ghc_minor="${ghc_minor%%.*}" 370 | 371 | local gmp_name gmp_file tinfo_file url 372 | gmp_name='' 373 | gmp_file='' 374 | tinfo_file='' 375 | case "${HALCYON_INTERNAL_PLATFORM}" in 376 | 'freebsd-10'*) 377 | url=$( map_ghc_version_to_freebsd_i386_url "${ghc_version}" ) || return 1 378 | ;; 379 | 'linux-centos-6'*) 380 | gmp_file='/usr/lib/libgmp.so.3' 381 | tinfo_file='/lib/libtinfo.so.5' 382 | if (( ghc_major < 7 || ghc_minor < 10 )); then 383 | gmp_name='libgmp.so.3' 384 | url=$( map_ghc_version_to_gmp4_linux_i386_url "${ghc_version}" ) || return 1 385 | else 386 | log_error "Unsupported GHC version for ${description}: ${ghc_version} (GMP 5)" 387 | log_error "To use ${description}, use GHC 7.8.4 (GMP 4) or older" 388 | return 1 389 | fi 390 | ;; 391 | 'linux-centos-7'*|'linux-fedora-19'*|'linux-fedora-20'*) 392 | gmp_file='/usr/lib/libgmp.so.10' 393 | tinfo_file='/usr/lib/libtinfo.so.5' 394 | if (( ghc_major < 7 || ghc_minor < 8 )); then 395 | gmp_name='libgmp.so.3' 396 | url=$( map_ghc_version_to_gmp4_linux_i386_url "${ghc_version}" ) || return 1 397 | else 398 | gmp_name='libgmp.so.10' 399 | url=$( map_ghc_version_to_gmp5_linux_i386_url "${ghc_version}" ) || return 1 400 | fi 401 | ;; 402 | 'linux-debian-6'*|'linux-ubuntu-10'*|'linux-sles-11'*) 403 | gmp_file='/usr/lib/libgmp.so.3' 404 | tinfo_file='/lib/libncurses.so.5' 405 | if (( ghc_major < 7 || ghc_minor < 10 )); then 406 | gmp_name='libgmp.so.3' 407 | url=$( map_ghc_version_to_gmp4_linux_i386_url "${ghc_version}" ) || return 1 408 | else 409 | log_error "Unsupported GHC version for ${description}: ${ghc_version} (GMP 5)" 410 | log_error "To use ${description}, use GHC 7.8.4 (GMP 4) or older" 411 | return 1 412 | fi 413 | ;; 414 | 'linux-debian-7'*|'linux-debian-8'*|'linux-ubuntu-14'*|'linux-ubuntu-15'*) 415 | gmp_file='/usr/lib/i386-linux-gnu/libgmp.so.10' 416 | tinfo_file='/lib/i386-linux-gnu/libtinfo.so.5' 417 | if (( ghc_major < 7 || ghc_minor < 8 )); then 418 | gmp_name='libgmp.so.3' 419 | url=$( map_ghc_version_to_gmp4_linux_i386_url "${ghc_version}" ) || return 1 420 | else 421 | gmp_name='libgmp.so.10' 422 | url=$( map_ghc_version_to_gmp5_linux_i386_url "${ghc_version}" ) || return 1 423 | fi 424 | ;; 425 | 'linux-ubuntu-12'*) 426 | tinfo_file='/lib/i386-linux-gnu/libtinfo.so.5' 427 | if (( ghc_major < 7 || ghc_minor < 8 )); then 428 | gmp_file='/usr/lib/libgmp.so.3' 429 | gmp_name='libgmp.so.3' 430 | url=$( map_ghc_version_to_gmp4_linux_i386_url "${ghc_version}" ) || return 1 431 | else 432 | gmp_file='/usr/lib/i386-linux-gnu/libgmp.so.10' 433 | gmp_name='libgmp.so.10' 434 | url=$( map_ghc_version_to_gmp5_linux_i386_url "${ghc_version}" ) || return 1 435 | fi 436 | ;; 437 | 'osx-'*) 438 | url=$( map_ghc_version_to_osx_i386_url "${ghc_version}" ) || return 1 439 | ;; 440 | *) 441 | log_error "Unsupported platform: ${description}" 442 | return 1 443 | esac 444 | 445 | symlink_ghc_libs "${gmp_name}" "${gmp_file}" "${tinfo_file}" || return 1 446 | 447 | echo "${url}" 448 | } 449 | 450 | 451 | symlink_ghc_x86_64_libs () { 452 | expect_vars HALCYON_INTERNAL_PLATFORM 453 | 454 | local tag 455 | expect_args tag -- "$@" 456 | 457 | local description 458 | description=$( format_platform_description "${HALCYON_INTERNAL_PLATFORM}" ) 459 | 460 | local ghc_version ghc_major ghc_minor 461 | ghc_version=$( get_tag_ghc_version "${tag}" ) 462 | ghc_major="${ghc_version%%.*}" 463 | ghc_minor="${ghc_version#*.}" 464 | ghc_minor="${ghc_minor%%.*}" 465 | 466 | local gmp_name gmp_file tinfo_file url 467 | gmp_name='' 468 | gmp_file='' 469 | tinfo_file='' 470 | case "${HALCYON_INTERNAL_PLATFORM}" in 471 | 'freebsd-10'*) 472 | url=$( map_ghc_version_to_freebsd_x86_64_url "${ghc_version}" ) || return 1 473 | ;; 474 | 'linux-amzn-2014.09'*|'linux-centos-6'*|'linux-rhel-6'*) 475 | gmp_file='/usr/lib64/libgmp.so.3' 476 | tinfo_file='/lib64/libtinfo.so.5' 477 | if (( ghc_major < 7 || ghc_minor < 10 )); then 478 | gmp_name='libgmp.so.3' 479 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 480 | else 481 | gmp_name='libgmp.so.10' 482 | url=$( map_ghc_version_to_gmp5_linux_x86_64_url "${ghc_version}" ) || return 1 483 | fi 484 | ;; 485 | 'linux-arch'*) 486 | gmp_file='/usr/lib/libgmp.so.10' 487 | tinfo_file='/usr/lib/libncurses.so.5' 488 | if (( ghc_major < 7 || ghc_minor < 8 )); then 489 | gmp_name='libgmp.so.3' 490 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 491 | else 492 | gmp_name='libgmp.so.10' 493 | url=$( map_ghc_version_to_gmp5_linux_x86_64_url "${ghc_version}" ) || return 1 494 | fi 495 | ;; 496 | 'linux-centos-7'*|'linux-fedora-19'*|'linux-fedora-20'*|'linux-fedora-21'*|'linux-rhel-7'*) 497 | gmp_file='/usr/lib64/libgmp.so.10' 498 | tinfo_file='/usr/lib64/libtinfo.so.5' 499 | if (( ghc_major < 7 || ghc_minor < 8 )); then 500 | gmp_name='libgmp.so.3' 501 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 502 | else 503 | gmp_name='libgmp.so.10' 504 | url=$( map_ghc_version_to_gmp5_linux_x86_64_url "${ghc_version}" ) || return 1 505 | fi 506 | ;; 507 | 'linux-debian-6'*|'linux-ubuntu-10'*) 508 | gmp_file='/usr/lib/libgmp.so.3' 509 | tinfo_file='/lib/libncurses.so.5' 510 | if (( ghc_major < 7 || ghc_minor < 10 )); then 511 | gmp_name='libgmp.so.3' 512 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 513 | else 514 | log_error "Unsupported GHC version for ${description}: ${ghc_version} (GMP 5)" 515 | log_error "To use ${description}, use GHC 7.8.4 (GMP 4) or older" 516 | return 1 517 | fi 518 | ;; 519 | 'linux-debian-7'*|'linux-debian-8'*|'linux-ubuntu-14'*|'linux-ubuntu-15'*) 520 | gmp_file='/usr/lib/x86_64-linux-gnu/libgmp.so.10' 521 | tinfo_file='/lib/x86_64-linux-gnu/libtinfo.so.5' 522 | if (( ghc_major < 7 || ghc_minor < 8 )); then 523 | gmp_name='libgmp.so.3' 524 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 525 | else 526 | gmp_name='libgmp.so.10' 527 | url=$( map_ghc_version_to_gmp5_linux_x86_64_url "${ghc_version}" ) || return 1 528 | fi 529 | ;; 530 | 'linux-exherbo'*|'linux-gentoo'*) 531 | gmp_file='/usr/lib64/libgmp.so.10' 532 | tinfo_file='/lib64/libncurses.so.5' 533 | if (( ghc_major < 7 || ghc_minor < 8 )); then 534 | gmp_name='libgmp.so.3' 535 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 536 | else 537 | gmp_name='libgmp.so.10' 538 | url=$( map_ghc_version_to_gmp5_linux_x86_64_url "${ghc_version}" ) || return 1 539 | fi 540 | ;; 541 | 'linux-opensuse-13'*|'linux-sles-12'*) 542 | gmp_file='/usr/lib64/libgmp.so.10' 543 | tinfo_file='/lib64/libtinfo.so.5' 544 | if (( ghc_major < 7 || ghc_minor < 8 )); then 545 | gmp_name='libgmp.so.3' 546 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 547 | else 548 | gmp_name='libgmp.so.10' 549 | url=$( map_ghc_version_to_gmp5_linux_x86_64_url "${ghc_version}" ) || return 1 550 | fi 551 | ;; 552 | 'linux-slackware-14'*) 553 | tinfo_file='/lib64/libncurses.so.5' 554 | if (( ghc_major < 7 || ghc_minor < 8 )); then 555 | gmp_file='/usr/lib64/libgmp.so.3' 556 | gmp_name='libgmp.so.3' 557 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 558 | else 559 | gmp_file='/usr/lib64/libgmp.so.10' 560 | gmp_name='libgmp.so.10' 561 | url=$( map_ghc_version_to_gmp5_linux_x86_64_url "${ghc_version}" ) || return 1 562 | fi 563 | ;; 564 | 'linux-sles-11'*) 565 | gmp_file='/usr/lib64/libgmp.so.3' 566 | tinfo_file='/lib64/libncurses.so.5' 567 | if (( ghc_major < 7 || ghc_minor < 10 )); then 568 | gmp_name='libgmp.so.3' 569 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 570 | else 571 | log_error "Unsupported GHC version for ${description}: ${ghc_version} (GMP 5)" 572 | log_error "To use ${description}, use GHC 7.8.4 (GMP 4) or older" 573 | return 1 574 | fi 575 | ;; 576 | 'linux-ubuntu-12'*) 577 | tinfo_file='/lib/x86_64-linux-gnu/libtinfo.so.5' 578 | if (( ghc_major < 7 || ghc_minor < 8 )); then 579 | gmp_file='/usr/lib/libgmp.so.3' 580 | gmp_name='libgmp.so.3' 581 | url=$( map_ghc_version_to_gmp4_linux_x86_64_url "${ghc_version}" ) || return 1 582 | else 583 | gmp_file='/usr/lib/x86_64-linux-gnu/libgmp.so.10' 584 | gmp_name='libgmp.so.10' 585 | url=$( map_ghc_version_to_gmp5_linux_x86_64_url "${ghc_version}" ) || return 1 586 | fi 587 | ;; 588 | 'osx-'*) 589 | url=$( map_ghc_version_to_osx_x86_64_url "${ghc_version}" ) || return 1 590 | ;; 591 | *) 592 | log_error "Unsupported platform: ${description}" 593 | return 1 594 | esac 595 | 596 | symlink_ghc_libs "${gmp_name}" "${gmp_file}" "${tinfo_file}" || return 1 597 | 598 | echo "${url}" 599 | } 600 | 601 | 602 | build_ghc_dir () { 603 | expect_vars HALCYON_BASE \ 604 | HALCYON_GHC_NO_REMOVE_DOC HALCYON_GHC_NO_STRIP \ 605 | HALCYON_INTERNAL_PLATFORM 606 | 607 | local tag source_dir 608 | expect_args tag source_dir -- "$@" 609 | 610 | if ! rm -rf "${HALCYON_BASE}/ghc"; then 611 | log_error 'Failed to prepare GHC directory' 612 | return 1 613 | fi 614 | 615 | local ghc_version ghc_dir 616 | ghc_version=$( get_tag_ghc_version "${tag}" ) 617 | ghc_dir=$( get_tmp_dir "ghc-${ghc_version}" ) || return 1 618 | 619 | log 'Building GHC directory' 620 | 621 | # NOTE: There is no libgmp.so.3 on some platforms, and there is no 622 | # .10-flavoured binary distribution of GHC < 7.8. However, GHC does 623 | # not use the `mpn_bdivmod` function, which is the only difference 624 | # between the ABI of .3 and .10. Hence, on some platforms, .10 is 625 | # symlinked to .3, and the .3-flavoured binary distribution is used. 626 | local ghc_original_url 627 | case "${HALCYON_INTERNAL_PLATFORM}" in 628 | *'-i386') 629 | ghc_original_url=$( symlink_ghc_i386_libs "${tag}" ) || return 1 630 | ;; 631 | *'-x86_64') 632 | ghc_original_url=$( symlink_ghc_x86_64_libs "${tag}" ) || return 1 633 | ;; 634 | *) 635 | local description 636 | description=$( format_platform_description "${HALCYON_INTERNAL_PLATFORM}" ) 637 | 638 | log_error "Unsupported platform: ${description}" 639 | return 1 640 | esac 641 | 642 | acquire_original_source "${ghc_original_url}" "${ghc_dir}" || return 1 643 | 644 | local ghc_sub_dir 645 | if ! ghc_sub_dir=$( 646 | find_tree "${ghc_dir}" -type d -maxdepth 1 -name 'ghc-*' | 647 | match_exactly_one 648 | ); then 649 | log_error 'Failed to detect GHC source directory' 650 | return 1 651 | fi 652 | 653 | local ghc_build_dir 654 | ghc_build_dir="${ghc_dir}/${ghc_sub_dir}" 655 | expect_existing "${ghc_build_dir}" || return 1 656 | 657 | if [[ -f "${source_dir}/.halcyon/ghc-pre-build-hook" ]]; then 658 | log 'Executing GHC pre-build hook' 659 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 660 | HALCYON_GHC_VERSION="${ghc_version}" \ 661 | "${source_dir}/.halcyon/ghc-pre-build-hook" \ 662 | "${tag}" "${source_dir}" "${ghc_build_dir}" 2>&1 | quote 663 | then 664 | log_error 'Failed to execute GHC pre-build hook' 665 | return 1 666 | fi 667 | log 'GHC pre-build hook executed' 668 | fi 669 | 670 | local cc make 671 | local -a opts_a 672 | cc='' 673 | make='make' 674 | opts_a=() 675 | opts_a+=( --prefix="${HALCYON_BASE}/ghc" ) 676 | 677 | case "${HALCYON_INTERNAL_PLATFORM}" in 678 | 'freebsd-'*) 679 | # NOTE: As described in: 680 | # https://downloads.haskell.org/~ghc/7.8.4/README.fbsd.html 681 | cc='gcc48' 682 | make='gmake' 683 | opts_a+=( --with-gcc='gcc48' ) 684 | opts_a+=( --with-ld='/usr/local/bin/ld' ) 685 | ;; 686 | esac 687 | 688 | log 'Installing GHC' 689 | 690 | local installed_size 691 | if ! ( 692 | cd "${ghc_build_dir}" && 693 | CC="${cc}" \ 694 | ./configure "${opts_a[@]}" 2>&1 | quote && 695 | "${make}" install 2>&1 | quote 696 | ) || 697 | ! copy_ghc_magic "${source_dir}" || 698 | ! installed_size=$( get_size "${HALCYON_BASE}/ghc" ) 699 | then 700 | log_error 'Failed to install GHC' 701 | return 1 702 | fi 703 | log "GHC installed, ${installed_size}" 704 | 705 | if [[ -f "${source_dir}/.halcyon/ghc-post-build-hook" ]]; then 706 | log 'Executing GHC post-build hook' 707 | if ! HALCYON_INTERNAL_RECURSIVE=1 \ 708 | HALCYON_GHC_VERSION="${ghc_version}" \ 709 | "${source_dir}/.halcyon/ghc-post-build-hook" \ 710 | "${tag}" "${source_dir}" "${ghc_build_dir}" 2>&1 | quote 711 | then 712 | log_error 'Failed to execute GHC post-build hook' 713 | return 1 714 | fi 715 | log 'GHC post-build hook executed' 716 | fi 717 | 718 | if ! (( HALCYON_GHC_NO_REMOVE_DOC )) && 719 | [[ -d "${HALCYON_BASE}/ghc/share/doc" ]] 720 | then 721 | log_indent_begin 'Removing documentation from GHC directory...' 722 | 723 | local trimmed_size 724 | if ! rm -rf "${HALCYON_BASE}/ghc/share/doc" || 725 | ! trimmed_size=$( get_size "${HALCYON_BASE}/ghc" ) 726 | then 727 | log_indent_end 'error' 728 | return 1 729 | fi 730 | log_indent_end "done, ${trimmed_size}" 731 | fi 732 | 733 | if ! (( HALCYON_GHC_NO_STRIP )); then 734 | # NOTE: On OS X, stripping GHC binary distributions with 735 | # object splitting supported can take a very long time. 736 | # https://github.com/mietek/halcyon/issues/43 737 | case "${HALCYON_INTERNAL_PLATFORM}" in 738 | 'osx-'*) 739 | if ghc --info | grep -qE '"Object splitting( supported)?","YES"'; then 740 | log_warning 'Split objects detected' 741 | log_warning 'Expected time to strip GHC directory: 30-120 minutes' 742 | log_warning 'To disable stripping GHC directory, set HALCYON_GHC_NO_STRIP to 1' 743 | fi 744 | esac 745 | 746 | log_indent_begin 'Stripping GHC directory...' 747 | 748 | local stripped_size 749 | if ! strip_tree "${HALCYON_BASE}/ghc" || 750 | ! stripped_size=$( get_size "${HALCYON_BASE}/ghc" ) 751 | then 752 | log_indent_end 'error' 753 | return 1 754 | fi 755 | log_indent_end "done, ${stripped_size}" 756 | fi 757 | 758 | if ! derive_ghc_tag "${tag}" >"${HALCYON_BASE}/ghc/.halcyon-tag"; then 759 | log_error 'Failed to write GHC tag' 760 | return 1 761 | fi 762 | } 763 | 764 | 765 | archive_ghc_dir () { 766 | expect_vars HALCYON_BASE HALCYON_NO_ARCHIVE \ 767 | HALCYON_INTERNAL_PLATFORM 768 | 769 | if (( HALCYON_NO_ARCHIVE )); then 770 | return 0 771 | fi 772 | 773 | expect_existing "${HALCYON_BASE}/ghc/.halcyon-tag" || return 1 774 | 775 | local ghc_tag archive_name 776 | ghc_tag=$( detect_ghc_tag "${HALCYON_BASE}/ghc/.halcyon-tag") || return 1 777 | archive_name=$( format_ghc_archive_name "${ghc_tag}" ) 778 | 779 | log 'Archiving GHC directory' 780 | 781 | create_cached_archive "${HALCYON_BASE}/ghc" "${archive_name}" || return 1 782 | upload_cached_file "${HALCYON_INTERNAL_PLATFORM}" "${archive_name}" || return 1 783 | } 784 | 785 | 786 | validate_ghc_dir () { 787 | expect_vars HALCYON_BASE 788 | 789 | local tag 790 | expect_args tag -- "$@" 791 | 792 | local ghc_tag 793 | ghc_tag=$( derive_ghc_tag "${tag}" ) 794 | detect_tag "${HALCYON_BASE}/ghc/.halcyon-tag" "${ghc_tag//./\.}" || return 1 795 | } 796 | 797 | 798 | restore_ghc_dir () { 799 | expect_vars HALCYON_BASE \ 800 | HALCYON_INTERNAL_PLATFORM 801 | 802 | local tag 803 | expect_args tag -- "$@" 804 | 805 | local archive_name 806 | archive_name=$( format_ghc_archive_name "${tag}" ) 807 | 808 | if validate_ghc_dir "${tag}" >'/dev/null'; then 809 | log 'Using existing GHC directory' 810 | 811 | touch_cached_file "${archive_name}" 812 | return 0 813 | fi 814 | rm -rf "${HALCYON_BASE}/ghc" || true 815 | 816 | log 'Restoring GHC directory' 817 | 818 | if ! extract_cached_archive_over "${archive_name}" "${HALCYON_BASE}/ghc" || 819 | ! validate_ghc_dir "${tag}" >'/dev/null' 820 | then 821 | rm -rf "${HALCYON_BASE}/ghc" || true 822 | cache_stored_file "${HALCYON_INTERNAL_PLATFORM}" "${archive_name}" || return 1 823 | 824 | if ! extract_cached_archive_over "${archive_name}" "${HALCYON_BASE}/ghc" || 825 | ! validate_ghc_dir "${tag}" >'/dev/null' 826 | then 827 | rm -rf "${HALCYON_BASE}/ghc" || true 828 | 829 | log_warning 'Failed to restore GHC directory' 830 | return 1 831 | fi 832 | else 833 | touch_cached_file "${archive_name}" 834 | fi 835 | } 836 | 837 | 838 | recache_ghc_package_db () { 839 | if ! ghc-pkg recache --global 2>&1 | quote; then 840 | log_warning 'Failed to recache GHC package database' 841 | fi 842 | } 843 | 844 | 845 | install_ghc_dir () { 846 | expect_vars HALCYON_NO_BUILD HALCYON_NO_BUILD_DEPENDENCIES \ 847 | HALCYON_GHC_REBUILD 848 | 849 | local tag source_dir 850 | expect_args tag source_dir -- "$@" 851 | 852 | if ! (( HALCYON_GHC_REBUILD )); then 853 | if restore_ghc_dir "${tag}"; then 854 | recache_ghc_package_db 855 | return 0 856 | fi 857 | 858 | # NOTE: Returns 2 if build is needed. 859 | if (( HALCYON_NO_BUILD )) || (( HALCYON_NO_BUILD_DEPENDENCIES )); then 860 | log_error 'Cannot build GHC directory' 861 | return 2 862 | fi 863 | fi 864 | 865 | build_ghc_dir "${tag}" "${source_dir}" || return 1 866 | recache_ghc_package_db 867 | archive_ghc_dir || return 1 868 | } 869 | --------------------------------------------------------------------------------