packages feed

haskell-ci-0.12: src/HaskellCI/Bash/Template.hs

{-# LANGUAGE CPP #-}
module HaskellCI.Bash.Template (
    -- * Input
    Z (..),
    defaultZ,
    -- * Rendering
    renderIO,
    render,
) where

import HaskellCI.Prelude

import Control.Monad (forM_)

import qualified Zinza

data Z = Z
    { zJobs      :: [String]
    , zRegendata :: String
    , zBlocks    :: [String]
    , zTestsCond :: String
    , zBenchCond :: String
    , zApt       :: [String]
    , zNotNull   :: [String] -> Bool
    , zUnwords   :: [String] -> String
    }
  deriving (Generic)

defaultZ :: Z
defaultZ = Z
    { zJobs      = []
    , zRegendata = "[]"
    , zBlocks    = []
    , zTestsCond = "1"
    , zBenchCond = "1"
    , zApt       = []
    , zNotNull   = not . null
    , zUnwords   = unwords
    }

instance Zinza.Zinza Z where
    toType    = Zinza.genericToTypeSFP
    toValue   = Zinza.genericToValueSFP
    fromValue = Zinza.genericFromValueSFP

-------------------------------------------------------------------------------
-- Development
-------------------------------------------------------------------------------

-- #define DEVELOPMENT 1

#ifdef DEVELOPMENT

renderIO :: Z -> IO String
renderIO z = do
    template <- Zinza.parseAndCompileTemplateIO "haskell-ci.sh.zinza"
    template z

#endif

-------------------------------------------------------------------------------
-- Production
-------------------------------------------------------------------------------

#ifndef DEVELOPMENT
renderIO :: Z -> IO String
renderIO = return . render
#endif

type Writer a = (String, a)
tell :: String -> Writer (); tell x = (x, ())
execWriter :: Writer a -> String; execWriter = fst

-- To generate:
--
-- :m *HaskellCI.Bash.Template
-- Zinza.parseAndCompileModuleIO (Zinza.simpleConfig "Module" [] :: Zinza.ModuleConfig Z) "haskell-ci.sh.zinza" >>= putStr

render :: Z -> String
render z_root = execWriter $ do
  tell "#!/bin/bash\n"
  tell "# shellcheck disable=SC2086,SC2016,SC2046\n"
  tell "# REGENDATA "
  tell (zRegendata z_root)
  tell "\n"
  tell "\n"
  tell "set -o pipefail\n"
  tell "\n"
  tell "# Mode\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "if [ \"$1\" = \"indocker\" ]; then\n"
  tell "    INDOCKER=true\n"
  tell "    shift\n"
  tell "else\n"
  tell "    INDOCKER=false\n"
  tell "fi\n"
  tell "\n"
  tell "# Run configuration\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "CFG_CABAL_STORE_CACHE=\"\"\n"
  tell "CFG_CABAL_REPO_CACHE=\"\"\n"
  tell "CFG_JOBS=\""
  tell (zUnwords z_root (zJobs z_root))
  tell "\"\n"
  tell "CFG_CABAL_UPDATE=false\n"
  tell "\n"
  tell "SCRIPT_NAME=$(basename \"$0\")\n"
  tell "START_TIME=\"$(date +'%s')\"\n"
  tell "\n"
  tell "XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}\n"
  tell "\n"
  tell "# Job configuration\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "GHC_VERSION=\"non-existing\"\n"
  tell "CABAL_VERSION=3.2\n"
  tell "HEADHACKAGE=false\n"
  tell "\n"
  tell "# Locale\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "export LC_ALL=C.UTF-8\n"
  tell "\n"
  tell "# Utilities\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "SGR_RED='\\033[1;31m'\n"
  tell "SGR_GREEN='\\033[1;32m'\n"
  tell "SGR_BLUE='\\033[1;34m'\n"
  tell "SGR_CYAN='\\033[1;96m'\n"
  tell "SGR_RESET='\\033[0m' # No Color\n"
  tell "\n"
  tell "put_info() {\n"
  tell "    printf \"$SGR_CYAN%s$SGR_RESET\\n\" \"### $*\"\n"
  tell "}\n"
  tell "\n"
  tell "put_error() {\n"
  tell "    printf \"$SGR_RED%s$SGR_RESET\\n\" \"!!! $*\"\n"
  tell "}\n"
  tell "\n"
  tell "run_cmd() {\n"
  tell "    local PRETTYCMD=\"$*\"\n"
  tell "    local PROMPT\n"
  tell "    if $INDOCKER; then\n"
  tell "        PROMPT=\"$(pwd) >>>\"\n"
  tell "    else\n"
  tell "        PROMPT=\">>>\"\n"
  tell "    fi\n"
  tell "\n"
  tell "    printf \"$SGR_BLUE%s %s$SGR_RESET\\n\" \"$PROMPT\" \"$PRETTYCMD\"\n"
  tell "\n"
  tell "    local start_time end_time cmd_duration total_duration\n"
  tell "    start_time=$(date +'%s')\n"
  tell "\n"
  tell "    \"$@\"\n"
  tell "    local RET=$?\n"
  tell "\n"
  tell "    end_time=$(date +'%s')\n"
  tell "    cmd_duration=$((end_time - start_time))\n"
  tell "    total_duration=$((end_time - START_TIME))\n"
  tell "\n"
  tell "    cmd_min=$((cmd_duration / 60))\n"
  tell "    cmd_sec=$((cmd_duration % 60))\n"
  tell "\n"
  tell "    total_min=$((total_duration / 60))\n"
  tell "    total_sec=$((total_duration % 60))\n"
  tell "\n"
  tell "    if [ $RET -eq 0 ]; then\n"
  tell "        printf \"$SGR_GREEN%s$SGR_RESET (%dm%02ds; %dm%02ds)\\n\" \"<<< $PRETTYCMD\" \"$cmd_min\" \"$cmd_sec\" \"$total_min\" \"$total_sec\"\n"
  tell "    else\n"
  tell "        printf \"$SGR_RED%s$SGR_RESET\\n\" \"!!! $PRETTYCMD\"\n"
  tell "        exit 1\n"
  tell "    fi\n"
  tell "}\n"
  tell "\n"
  tell "run_cmd_if() {\n"
  tell "    local COND=$1\n"
  tell "    shift\n"
  tell "\n"
  tell "    if [ $COND -eq 1 ]; then\n"
  tell "        run_cmd \"$@\"\n"
  tell "    else\n"
  tell "        local PRETTYCMD=\"$*\"\n"
  tell "        local PROMPT\n"
  tell "        PROMPT=\"$(pwd) (skipping) >>>\"\n"
  tell "\n"
  tell "        printf \"$SGR_BLUE%s %s$SGR_RESET\\n\" \"$PROMPT\" \"$PRETTYCMD\"\n"
  tell "    fi\n"
  tell "}\n"
  tell "\n"
  tell "run_cmd_unchecked() {\n"
  tell "    local PRETTYCMD=\"$*\"\n"
  tell "    local PROMPT\n"
  tell "    if $INDOCKER; then\n"
  tell "        PROMPT=\"$(pwd) >>>\"\n"
  tell "    else\n"
  tell "        PROMPT=\">>>\"\n"
  tell "    fi\n"
  tell "\n"
  tell "    printf \"$SGR_BLUE%s %s$SGR_RESET\\n\" \"$PROMPT\" \"$PRETTYCMD\"\n"
  tell "\n"
  tell "    local start_time end_time cmd_duration total_duration cmd_min cmd_sec total_min total_sec\n"
  tell "    start_time=$(date +'%s')\n"
  tell "\n"
  tell "    \"$@\"\n"
  tell "\n"
  tell "    end_time=$(date +'%s')\n"
  tell "    cmd_duration=$((end_time - start_time))\n"
  tell "    total_duration=$((end_time - START_TIME))\n"
  tell "\n"
  tell "    cmd_min=$((cmd_duration / 60))\n"
  tell "    cmd_sec=$((cmd_duration % 60))\n"
  tell "\n"
  tell "    total_min=$((total_duration / 60))\n"
  tell "    total_sec=$((total_duration % 60))\n"
  tell "\n"
  tell "    printf \"$SGR_GREEN%s$SGR_RESET (%dm%02ds; %dm%02ds)\\n\" \"<<< $PRETTYCMD\" \"$cmd_min\" \"$cmd_sec\" \"$total_min\" \"$total_sec\"\n"
  tell "}\n"
  tell "\n"
  tell "change_dir() {\n"
  tell "    local DIR=$1\n"
  tell "    if [ -d \"$DIR\" ]; then\n"
  tell "        printf \"$SGR_BLUE%s$SGR_RESET\\n\" \"change directory to $DIR\"\n"
  tell "        cd \"$DIR\" || exit 1\n"
  tell "    else\n"
  tell "        printf \"$SGR_RED%s$SGR_RESET\\n\" \"!!! cd $DIR\"\n"
  tell "        exit 1\n"
  tell "    fi\n"
  tell "}\n"
  tell "\n"
  tell "change_dir_if() {\n"
  tell "    local COND=$1\n"
  tell "    local DIR=$2\n"
  tell "\n"
  tell "    if [ $COND -ne 0 ]; then\n"
  tell "        change_dir \"$DIR\"\n"
  tell "    fi\n"
  tell "}\n"
  tell "\n"
  tell "echo_to() {\n"
  tell "    local DEST=$1\n"
  tell "    local CONTENTS=$2\n"
  tell "\n"
  tell "    echo \"$CONTENTS\" >> \"$DEST\"\n"
  tell "}\n"
  tell "\n"
  tell "echo_if_to() {\n"
  tell "    local COND=$1\n"
  tell "    local DEST=$2\n"
  tell "    local CONTENTS=$3\n"
  tell "\n"
  tell "    if [ $COND -ne 0 ]; then\n"
  tell "        echo_to \"$DEST\" \"$CONTENTS\"\n"
  tell "    fi\n"
  tell "}\n"
  tell "\n"
  tell "install_cabalplan() {\n"
  tell "    put_info \"installing cabal-plan\"\n"
  tell "\n"
  tell "    if [ ! -e $CABAL_REPOCACHE/downloads/cabal-plan ]; then\n"
  tell "        curl -L https://github.com/haskell-hvr/cabal-plan/releases/download/v0.6.2.0/cabal-plan-0.6.2.0-x86_64-linux.xz > /tmp/cabal-plan.xz || exit 1\n"
  tell "        (cd /tmp && echo \"de73600b1836d3f55e32d80385acc055fd97f60eaa0ab68a755302685f5d81bc  cabal-plan.xz\" | sha256sum -c -)|| exit 1\n"
  tell "        mkdir -p $CABAL_REPOCACHE/downloads\n"
  tell "        xz -d < /tmp/cabal-plan.xz > $CABAL_REPOCACHE/downloads/cabal-plan || exit 1\n"
  tell "        chmod a+x $CABAL_REPOCACHE/downloads/cabal-plan || exit 1\n"
  tell "    fi\n"
  tell "\n"
  tell "    mkdir -p $CABAL_DIR/bin || exit 1\n"
  tell "    ln -s $CABAL_REPOCACHE/downloads/cabal-plan $CABAL_DIR/bin/cabal-plan || exit 1\n"
  tell "}\n"
  tell "\n"
  tell "# Help\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "show_usage() {\n"
  tell "cat <<EOF\n"
  tell "./haskell-ci.sh - build & test\n"
  tell "\n"
  tell "Usage: ./haskell-ci.sh [options]\n"
  tell "  A script to run automated checks locally (using Docker)\n"
  tell "\n"
  tell "Available options:\n"
  tell "  --jobs JOBS               Jobs to run (default: $CFG_JOBS)\n"
  tell "  --cabal-store-cache PATH  Directory to use for cabal-store-cache\n"
  tell "  --cabal-repo-cache PATH   Directory to use for cabal-repo-cache\n"
  tell "  --skip-cabal-update       Skip cabal update (useful with --cabal-repo-cache)\n"
  tell "  --no-skip-cabal-update\n"
  tell "  --help                    Print this message\n"
  tell "\n"
  tell "EOF\n"
  tell "}\n"
  tell "\n"
  tell "# getopt\n"
  tell "#######################################################################\n"
  tell "\n"
  tell "process_cli_options() {\n"
  tell "    while [ $# -gt 0 ]; do\n"
  tell "        arg=$1\n"
  tell "        case $arg in\n"
  tell "            --help)\n"
  tell "                show_usage\n"
  tell "                exit\n"
  tell "                ;;\n"
  tell "            --jobs)\n"
  tell "                CFG_JOBS=$2\n"
  tell "                shift\n"
  tell "                shift\n"
  tell "                ;;\n"
  tell "            --cabal-store-cache)\n"
  tell "                CFG_CABAL_STORE_CACHE=$2\n"
  tell "                shift\n"
  tell "                shift\n"
  tell "                ;;\n"
  tell "            --cabal-repo-cache)\n"
  tell "                CFG_CABAL_REPO_CACHE=$2\n"
  tell "                shift\n"
  tell "                shift\n"
  tell "                ;;\n"
  tell "            --skip-cabal-update)\n"
  tell "                CFG_CABAL_UPDATE=false\n"
  tell "                shift\n"
  tell "                ;;\n"
  tell "            --no-skip-cabal-update)\n"
  tell "                CFG_CABAL_UPDATE=true\n"
  tell "                shift\n"
  tell "                ;;\n"
  tell "            *)\n"
  tell "                echo \"Unknown option $arg\"\n"
  tell "                exit 1\n"
  tell "        esac\n"
  tell "    done\n"
  tell "}\n"
  tell "\n"
  tell "process_indocker_options () {\n"
  tell "    while [ $# -gt 0 ]; do\n"
  tell "        arg=$1\n"
  tell "\n"
  tell "        case $arg in\n"
  tell "            --ghc-version)\n"
  tell "                GHC_VERSION=$2\n"
  tell "                shift\n"
  tell "                shift\n"
  tell "                ;;\n"
  tell "            --cabal-version)\n"
  tell "                CABAL_VERSION=$2\n"
  tell "                shift\n"
  tell "                shift\n"
  tell "                ;;\n"
  tell "            --start-time)\n"
  tell "                START_TIME=$2\n"
  tell "                shift\n"
  tell "                shift\n"
  tell "                ;;\n"
  tell "            --cabal-update)\n"
  tell "                CABAL_UPDATE=$2\n"
  tell "                shift\n"
  tell "                shift\n"
  tell "                ;;\n"
  tell "            *)\n"
  tell "                echo \"Unknown option $arg\"\n"
  tell "                exit 1\n"
  tell "        esac\n"
  tell "    done\n"
  tell "}\n"
  tell "\n"
  tell "if $INDOCKER; then\n"
  tell "    process_indocker_options \"$@\"\n"
  tell "\n"
  tell "else\n"
  tell "    if [ -f \"$XDG_CONFIG_HOME/haskell-ci/bash.config\" ]; then\n"
  tell "        process_cli_options $(cat \"$XDG_CONFIG_HOME/haskell-ci/bash.config\")\n"
  tell "    fi\n"
  tell "\n"
  tell "    process_cli_options \"$@\"\n"
  tell "\n"
  tell "    put_info \"jobs:              $CFG_JOBS\"\n"
  tell "    put_info \"cabal-store-cache: $CFG_CABAL_STORE_CACHE\"\n"
  tell "    put_info \"cabal-repo-cache:  $CFG_CABAL_REPO_CACHE\"\n"
  tell "    put_info \"cabal-update:      $CFG_CABAL_UPDATE\"\n"
  tell "fi\n"
  tell "\n"
  tell "# Constants\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "SRCDIR=/hsci/src\n"
  tell "BUILDDIR=/hsci/build\n"
  tell "CABAL_DIR=\"$BUILDDIR/cabal\"\n"
  tell "CABAL_REPOCACHE=/hsci/cabal-repocache\n"
  tell "CABAL_STOREDIR=/hsci/store\n"
  tell "\n"
  tell "# Docker invoke\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "# if cache directory is specified, use it.\n"
  tell "# Otherwise use another tmpfs host\n"
  tell "if [ -z \"$CFG_CABAL_STORE_CACHE\" ]; then\n"
  tell "    CABALSTOREARG=\"--tmpfs $CABAL_STOREDIR:exec\"\n"
  tell "else\n"
  tell "    CABALSTOREARG=\"--volume $CFG_CABAL_STORE_CACHE:$CABAL_STOREDIR\"\n"
  tell "fi\n"
  tell "\n"
  tell "if [ -z \"$CFG_CABAL_REPO_CACHE\" ]; then\n"
  tell "    CABALREPOARG=\"--tmpfs $CABAL_REPOCACHE:exec\"\n"
  tell "else\n"
  tell "    CABALREPOARG=\"--volume $CFG_CABAL_REPO_CACHE:$CABAL_REPOCACHE\"\n"
  tell "fi\n"
  tell "\n"
  tell "echo_docker_cmd() {\n"
  tell "    local GHCVER=$1\n"
  tell "\n"
  tell "    # TODO: mount /hsci/src:ro (readonly)\n"
  tell "    echo docker run \\\n"
  tell "        --tty \\\n"
  tell "        --interactive \\\n"
  tell "        --rm \\\n"
  tell "        --label haskell-ci \\\n"
  tell "        --volume \"$(pwd):/hsci/src\" \\\n"
  tell "        $CABALSTOREARG \\\n"
  tell "        $CABALREPOARG \\\n"
  tell "        --tmpfs /tmp:exec \\\n"
  tell "        --tmpfs /hsci/build:exec \\\n"
  tell "        --workdir /hsci/build \\\n"
  tell "        \"phadej/ghc:$GHCVER-bionic\" \\\n"
  tell "        \"/bin/bash\" \"/hsci/src/$SCRIPT_NAME\" indocker \\\n"
  tell "        --ghc-version \"$GHCVER\" \\\n"
  tell "        --cabal-update \"$CFG_CABAL_UPDATE\" \\\n"
  tell "        --start-time \"$START_TIME\"\n"
  tell "}\n"
  tell "\n"
  tell "# if we are not in docker, loop through jobs\n"
  tell "if ! $INDOCKER; then\n"
  tell "    for JOB in $CFG_JOBS; do\n"
  tell "        put_info \"Running in docker: $JOB\"\n"
  tell "        run_cmd $(echo_docker_cmd \"$JOB\")\n"
  tell "    done\n"
  tell "\n"
  tell "    run_cmd echo \"ALL OK\"\n"
  tell "    exit 0\n"
  tell "fi\n"
  tell "\n"
  tell "# Otherwise we are in docker, and the rest of script executes\n"
  tell "put_info \"In docker\"\n"
  tell "\n"
  tell "# Environment\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "GHCDIR=/opt/ghc/$GHC_VERSION\n"
  tell "\n"
  tell "HC=$GHCDIR/bin/ghc\n"
  tell "HCPKG=$GHCDIR/bin/ghc-pkg\n"
  tell "HADDOCK=$GHCDIR/bin/haddock\n"
  tell "\n"
  tell "CABAL=/opt/cabal/$CABAL_VERSION/bin/cabal\n"
  tell "\n"
  tell "CABAL=\"$CABAL -vnormal+nowrap\"\n"
  tell "\n"
  tell "export CABAL_DIR\n"
  tell "export CABAL_CONFIG=\"$BUILDDIR/cabal/config\"\n"
  tell "\n"
  tell "PATH=\"$CABAL_DIR/bin:$PATH\"\n"
  tell "\n"
  tell "# HCNUMVER\n"
  tell "HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\\d+)\\.(\\d+)\\.(\\d+)(\\.(\\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')\n"
  tell "GHCJSARITH=0\n"
  tell "\n"
  tell "put_info \"HCNUMVER: $HCNUMVER\"\n"
  tell "\n"
  tell "# Args for shorter/nicer commands\n"
  tell "if [ "
  tell (zTestsCond z_root)
  tell " -ne 0 ] ; then ARG_TESTS=--enable-tests; else ARG_TESTS=--disable-tests; fi\n"
  tell "if [ "
  tell (zBenchCond z_root)
  tell " -ne 0 ] ; then ARG_BENCH=--enable-benchmarks; else ARG_BENCH=--disable-benchmarks; fi\n"
  tell "ARG_COMPILER=\"--ghc --with-compiler=$HC\"\n"
  tell "\n"
  tell "put_info \"tests/benchmarks: $ARG_TESTS $ARG_BENCH\"\n"
  tell "\n"
  tell "# Apt dependencies\n"
  tell "##############################################################################\n"
  tell "\n"
  if (zNotNull z_root (zApt z_root))
  then do
    tell "run_cmd apt-get update\n"
    tell "run_cmd apt-get install -y --no-install-recommends "
    tell (zUnwords z_root (zApt z_root))
    tell "\n"
    return ()
  else do
    return ()
  tell "\n"
  tell "# Cabal config\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "mkdir -p $BUILDDIR/cabal\n"
  tell "\n"
  tell "cat > $BUILDDIR/cabal/config <<EOF\n"
  tell "remote-build-reporting: anonymous\n"
  tell "write-ghc-environment-files: always\n"
  tell "remote-repo-cache: $CABAL_REPOCACHE\n"
  tell "logs-dir:          $CABAL_DIR/logs\n"
  tell "world-file:        $CABAL_DIR/world\n"
  tell "extra-prog-path:   $CABAL_DIR/bin\n"
  tell "symlink-bindir:    $CABAL_DIR/bin\n"
  tell "installdir:        $CABAL_DIR/bin\n"
  tell "build-summary:     $CABAL_DIR/logs/build.log\n"
  tell "store-dir:         $CABAL_STOREDIR\n"
  tell "install-dirs user\n"
  tell "  prefix: $CABAL_DIR\n"
  tell "repository hackage.haskell.org\n"
  tell "  url: http://hackage.haskell.org/\n"
  tell "EOF\n"
  tell "\n"
  tell "if $HEADHACKAGE; then\n"
  tell "    put_error \"head.hackage is not implemented\"\n"
  tell "    exit 1\n"
  tell "fi\n"
  tell "\n"
  tell "run_cmd cat \"$BUILDDIR/cabal/config\"\n"
  tell "\n"
  tell "# Version\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "put_info \"Versions\"\n"
  tell "run_cmd $HC --version\n"
  tell "run_cmd_unchecked $HC --print-project-git-commit-id\n"
  tell "run_cmd $CABAL --version\n"
  tell "\n"
  tell "# Build script\n"
  tell "##############################################################################\n"
  tell "\n"
  tell "# update cabal index\n"
  tell "if $CABAL_UPDATE; then\n"
  tell "    put_info \"Updating Hackage index\"\n"
  tell "    run_cmd $CABAL v2-update -v\n"
  tell "fi\n"
  tell "\n"
  tell "# install cabal-plan\n"
  tell "install_cabalplan\n"
  tell "run_cmd cabal-plan --version\n"
  tell "\n"
  forM_ (zBlocks z_root) $ \z_var0_block -> do
    tell z_var0_block
    tell "\n"
  tell "\n"
  tell "# Done\n"
  tell "run_cmd echo OK\n"