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"