packages feed

ngx-export-distribution-0.6.0.2: nhm-tool.hs

{-# LANGUAGE RecordWildCards, LambdaCase, MultiWayIf, ViewPatterns #-}
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Text.Parsec hiding (uncons)
import Distribution.Simple.Program
import Distribution.Verbosity
import Cabal.Plan
import Control.Monad
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Text (Text)
import qualified Data.Map as M
import qualified Data.Set as S
import Data.Maybe
import Data.Char
import Data.List
import System.Console.ANSI
import System.Environment
import System.Directory
import System.FilePath
import System.Exit
import System.IO
import Data.Version (showVersion)
import Paths_ngx_export_distribution (version)

data DistData = DistData { distDataDir :: String
                         , distDataArchive :: String
                         , distDataTargetDir :: String
                         , distDataPatchOnly :: Bool
                         , distDataTargetLib :: String
                         , distDataOwnVerbosity :: Verbosity
                         , distDataOtherVerbosity :: Verbosity
                         , distDataWaitArg :: Maybe String
                         , distDataHelp :: Bool
                         }

defaultDistDataDir :: String
defaultDistDataDir = ".hslibs"

data PlanData = PlanData { planDataBuilddir :: String
                         , planDataQuery :: Maybe PlanDataQuery
                         , planDataWaitArg :: Maybe String
                         , planDataWaitProjectName :: Bool
                         , planDataHelp :: Bool
                         }

data PlanDataQuery = PlanDataQueryDeps String
                   | PlanDataQueryAbi

data InitData = InitData { initDataPrefix :: String
                         , initDataNoThreaded :: Bool
                         , initDataForce :: Bool
                         , initDataToStdout :: Bool
                         , initDataProject :: String
                         , initDataGhcOptions :: [String]
                         , initDataWaitArg :: Maybe String
                         , initDataHelp :: Bool
                         }

defaultInitDataPrefix :: String
defaultInitDataPrefix = "/var/lib/nginx"

data LddRec = LibHS String (Maybe FilePath)
            | LibFFI String (Maybe FilePath)
            | LibOther String (Maybe FilePath)
            deriving Show

data HelpSection = HelpDist | HelpPlan | HelpDeps | HelpInit deriving Eq

replaceChar :: Char -> Char -> String -> String
replaceChar from to = map $ \v -> if v == from then to else v

progVersion :: String
progVersion = "nhm-tool " ++ showVersion version

usage :: Maybe HelpSection -> Bool -> IO ()
usage section success = do
    when (isNothing section) $
        putStrLn $ progVersion ++
            ": help building custom Haskell handlers for Nginx,\n\
            \this is a tool from \
            \https://github.com/lyokha/nginx-haskell-module\n"
    putStrLn "Usage:"
    when (isNothing section || section == Just HelpDist) $
        T.putStrLn $ T.concat ["\n\
        \  * nhm-tool dist [-d dir -a ar | -p] [-t target_dir] [-v | -vv] \
        \lib\n\n\
        \    collect Haskell libraries on which 'lib' depends,\n\
        \    patch 'lib' to enable loading dependent libraries from \
        \'target_dir'\n\n\
        \    'dir' is a directory where dependent libraries will be collected\n\
        \      (default is ", T.pack defaultDistDataDir, ")\n\
        \    'target_dir' is a directory where dependent libraries will be\n\
        \      installed (no default, 'lib' will not be patched if omitted)\n\
        \    'ar' is the base name of the archive to contain 'lib' and\n\
        \      'dir' (no default, the archive will not be created if omitted)\n\
        \    if '-p' (patch-only) is specified then dependent libraries will\n\
        \      neither be collected nor archived\n\
        \    use options '-v' and '-vv' to increase verbosity level\n\
        \    special value '-' for 'dir', 'target_dir', and 'ar' resets them\n\
        \      (with -d- being equivalent to -p)"
        ]
    when (isNothing section || section `elem` map Just [HelpPlan, HelpDeps]) $
        T.putStrLn "\n\
        \  * nhm-tool plan [-d dir] [deps project-name | abi]\n\n\
        \    print build plan or its derivatives:\n\
        \    'deps' print all direct dependencies of 'project-name',\n\
        \      the output is compatible with the format of GHC environment \
        \files\n\
        \    'abi' print 'Arch-Os-CompilerId' bundle in GHC style\n\n\
        \    'dir' is the cabal build directory where the build plan is \
        \located\n\
        \      (default is dist-newstyle)"
    when (isNothing section || section == Just HelpDeps) $
        T.putStrLn "\n\
        \  * nhm-tool deps [-d dir] project-name\n\n\
        \    synonym for 'nhm-tool plan [-d dir] deps project-name'"
    when (isNothing section || section == Just HelpInit) $
        T.putStrLn $ T.concat ["\n\
        \  * nhm-tool init [-p dir] [-no-threaded] [-f | -to-stdout] \
        \project-name\n\n\
        \    bootstrap environment to build custom Haskell handlers\n\n\
        \    '-p' prefix for install dir, default is ",
        T.pack defaultInitDataPrefix, "\n\
        \    '-no-threaded' use base RTS library\n\
        \    '-f' force re-writing of existing files (except the source file)\n\
        \    '-to-stdout' do not write files but dump what will be written"
        ]
    when (isNothing section) $
        T.putStrLn "\n\
        \  * nhm-tool [-h | -help | --help | -v | -version | --version]\n\n\
        \    show this help message or version and exit,\n\
        \    help options are applicable in sub-commands as well"
    if success
        then exitSuccess
        else exitFailure

main :: IO ()
main = do
    args <- getArgs
    case args of
        "dist" : args' -> do
            let distData = foldl parseDistArg (Just defaultArgs) args'
                defaultArgs = DistData defaultDistDataDir "" "" False ""
                    normal normal Nothing False
            case distData of
                Nothing -> usage (Just HelpDist) False
                Just (normalizeDistData -> distData'@DistData {..}) ->
                    if | distDataHelp ->
                             usage (Just HelpDist) $ length args' == 1
                       | isJust distDataWaitArg
                         || null distDataTargetLib ->
                             usage (Just HelpDist) False
                       | otherwise -> cmdDist distData'
        "plan" : args' -> do
            let planData = foldl parsePlanArg (Just defaultArgs) args'
                defaultArgs = PlanData "" Nothing Nothing False False
            case planData of
                Nothing -> usage (Just HelpPlan) False
                Just planData'@PlanData {..} ->
                    if | planDataHelp ->
                             usage (Just HelpPlan) $ length args' == 1
                       | isJust planDataWaitArg
                         || planDataWaitProjectName ->
                             usage (Just HelpPlan) False
                       | otherwise -> cmdPlan planData'
        "deps" : args' -> do
            let planData = foldl parsePlanArg (Just defaultArgs) args'
                defaultArgs = PlanData "" Nothing Nothing True False
            case planData of
                Nothing -> usage (Just HelpDeps) False
                Just planData'@PlanData {..} ->
                    if | planDataHelp ->
                             usage (Just HelpDeps) $ length args' == 1
                       | isJust planDataWaitArg
                         || planDataWaitProjectName ->
                             usage (Just HelpDeps) False
                       | otherwise -> cmdPlan planData'
        "init" : args' -> do
            let initData = foldl parseInitArg (Just defaultArgs) args'
                defaultArgs = InitData defaultInitDataPrefix False False False
                    "" ["-Wall", "-O2"] Nothing False
            case initData of
                Nothing -> usage (Just HelpInit) False
                Just initData'@InitData {..} ->
                    if | initDataHelp ->
                             usage (Just HelpInit) $ length args' == 1
                       | isJust initDataWaitArg
                         || null initDataProject
                         || initDataForce && initDataToStdout ->
                             usage (Just HelpInit) False
                       | otherwise -> cmdInit initData'
        [arg] | arg `elem` ["-h", "-help", "--help"] ->
                  usage Nothing True
              | arg `elem` ["-v", "-version", "--version"] ->
                  putStrLn progVersion >> exitSuccess
        args' -> usage Nothing $ null args'
    where normalizeDistData dist@DistData { distDataDir = "-" } =
              normalizeDistData dist { distDataDir = defaultDistDataDir
                                     , distDataPatchOnly = True
                                     }
          normalizeDistData dist@DistData { distDataArchive = "-" } =
              normalizeDistData dist { distDataArchive = "" }
          normalizeDistData dist@DistData { distDataTargetDir = "-" } =
              normalizeDistData dist { distDataTargetDir = "" }
          normalizeDistData dist@DistData { distDataDir = "" } =
              normalizeDistData dist { distDataDir = defaultDistDataDir }
          normalizeDistData dist = dist

parseDistArg :: Maybe DistData -> String -> Maybe DistData
parseDistArg Nothing _ = Nothing
parseDistArg (Just dist@DistData {..}) arg =
    case distDataWaitArg of
        Nothing ->
            let (opt, value) = splitAt 2 arg
            in if | "-d" == opt ->
                        Just $ if null value
                                   then dist { distDataWaitArg = Just opt }
                                   else dist' { distDataDir = value }
                  | "-t" == opt ->
                        Just $ if null value
                                   then dist { distDataWaitArg = Just opt }
                                   else dist' { distDataTargetDir = value }
                  | "-a" == opt ->
                        Just $ if null value
                                   then dist { distDataWaitArg = Just opt }
                                   else dist' { distDataArchive = value }
                  | "-p" == arg ->
                        Just dist' { distDataPatchOnly = True }
                  | "-v" == arg ->
                        Just dist' { distDataOwnVerbosity = verbose }
                  | "-vv" == arg ->
                        Just dist' { distDataOwnVerbosity = verbose
                                   , distDataOtherVerbosity = verbose
                                   }
                  | (`elem` ["-h", "-help", "--help"]) arg ->
                        Just dist' { distDataHelp = True }
                  | Just '-' == (fst <$> uncons arg) ->
                        Nothing
                  | null distDataTargetLib ->
                        Just dist' { distDataTargetLib = arg }
                  | otherwise ->
                        Nothing
        Just "-d" -> Just dist' { distDataDir = arg }
        Just "-t" -> Just dist' { distDataTargetDir = arg }
        Just "-a" -> Just dist' { distDataArchive = arg }
        Just _ -> undefined
    where dist' = dist { distDataWaitArg = Nothing }

parsePlanArg :: Maybe PlanData -> String -> Maybe PlanData
parsePlanArg Nothing _ = Nothing
parsePlanArg (Just plan@PlanData {..}) arg =
    case planDataWaitArg of
        Nothing ->
            let (opt, value) = splitAt 2 arg
            in if | "deps" == arg && not hasDataQuery ->
                        Just plan' { planDataWaitProjectName = True }
                  | "abi" == arg && not hasDataQuery ->
                        Just plan' { planDataQuery = Just PlanDataQueryAbi }
                  | "-d" == opt ->
                        Just $ if null value
                                   then plan { planDataWaitArg = Just opt }
                                   else plan' { planDataBuilddir = value }
                  | (`elem` ["-h", "-help", "--help"]) arg ->
                        Just plan' { planDataHelp = True }
                  | Just '-' == (fst <$> uncons arg) ->
                        Nothing
                  | planDataWaitProjectName ->
                        Just plan'
                            { planDataQuery = Just $ PlanDataQueryDeps arg
                            , planDataWaitProjectName = False
                            }
                  | otherwise ->
                        Nothing
        Just "-d" -> Just plan' { planDataBuilddir = arg }
        Just _ -> undefined
    where plan' = plan { planDataWaitArg = Nothing }
          hasDataQuery = isJust planDataQuery || planDataWaitProjectName

parseInitArg :: Maybe InitData -> String -> Maybe InitData
parseInitArg Nothing _ = Nothing
parseInitArg (Just init'@InitData {..}) arg =
    case initDataWaitArg of
        Nothing ->
            let (opt, value) = splitAt 2 arg
            in if | "-p" == opt ->
                        Just $ if null value
                                   then init' { initDataWaitArg = Just opt }
                                   else init'' { initDataPrefix = value }
                  | "-no-threaded" == arg ->
                        Just init'' { initDataNoThreaded = True }
                  | "-f" == arg ->
                        Just init'' { initDataForce = True }
                  | "-to-stdout" == arg ->
                        Just init'' { initDataToStdout = True }
                  | (`elem` ["-h", "-help", "--help"]) arg ->
                        Just init'' { initDataHelp = True }
                  | Just '-' == (fst <$> uncons arg) ->
                        Nothing
                  | null initDataProject ->
                        Just init'' { initDataProject = arg }
                  | otherwise ->
                        Nothing
        Just "-p" -> Just init'' { initDataPrefix = arg }
        Just _ -> undefined
    where init'' = init' { initDataWaitArg = Nothing }

cmdDist :: DistData -> IO ()
cmdDist DistData {..} = do
    patchelf <- requireProgram' $ simpleProgram "patchelf"
    if distDataPatchOnly
        then patchTargetLib patchelf
        else do
            ldd <- requireProgram' $ simpleProgram "ldd"
            putStrLn' "---> Collecting libraries"
            lddOut <- getProgramOutput' ldd [distDataTargetLib]
            case parseLddOutput lddOut of
                Left err -> do
                    hPutStrLn stderr $ show err ++ " in\n" ++ lddOut
                    exitFailure
                Right recs -> do
                    tar' <- requireProgram' tarProgram
                    collectLibs recs lddOut
                    unless (null distDataTargetDir) $
                        putStrLn' "" >> patchTargetLib patchelf
                    unless (null distDataArchive) $
                        putStrLn' "" >> archiveLibs tar'
    where patchTargetLib patchelf = unless (null distDataTargetDir) $ do
              putStrLn' $ "---> Patching " ++ distDataTargetLib
              let printRpath = getProgramOutput'
                      patchelf ["--print-rpath", distDataTargetLib]
              patchelfOut <- printRpath
              case parsePatchelfRpathOutput patchelfOut of
                  Left err -> do
                      hPutStrLn stderr $ show err ++ " in\n" ++ patchelfOut
                      exitFailure
                  Right paths -> do
                      unless (distDataTargetDir `elem` paths) $ runProgram'
                          patchelf ["--set-rpath"
                                   ,distDataTargetDir ++ ':' : patchelfOut
                                   ,distDataTargetLib
                                   ]
                      printRpath >>= putStrLnTrim
          collectLibs recs lddOut = do
              let recsLibHS = M.fromList $
                      mapMaybe (\case
                                    LibHS name path -> Just (name, path)
                                    LibFFI name path -> Just (name, path)
                                    _ -> Nothing
                               ) recs
              if M.null recsLibHS
                  then do
                      hPutStrLn stderr $
                          "No Haskell libraries were collected in\n" ++ lddOut
                      exitFailure
                  else do
                      let (M.mapMaybe id -> recsLibHS', recsLibHSNotFound) =
                              M.partition isJust recsLibHS
                      if M.null recsLibHSNotFound
                          then do
                              createDirectoryIfMissing False distDataDir
                              forM_ (M.elems recsLibHS') $ \path -> do
                                  let dst = distDataDir </> takeFileName path
                                  putStrLn' $ path ++ " -> " ++ dst
                                  copyFile path dst
                          else do
                              hPutStrLn stderr $
                                 "Haskell libraries " ++
                                     show (M.keys recsLibHSNotFound) ++
                                         " were not found in\n" ++ lddOut
                              exitFailure
          archiveLibs tar' = unless (null distDataArchive) $ do
              putStrLn' "---> Archiving artifacts"
              tarOut <- getProgramOutput'
                  tar' ["czvf"
                       ,distDataArchive <.> ".tar.gz"
                       ,distDataTargetLib
                       ,distDataDir
                       ]
              putStrLnTrim tarOut
          requireProgram' = fmap fst .
              flip (requireProgram distDataOtherVerbosity) emptyProgramDb
          getProgramOutput' = getProgramOutput distDataOtherVerbosity
          runProgram' = runProgram distDataOtherVerbosity
          putStrLn' = when (distDataOwnVerbosity == verbose) . putStrLn
          putStrLnTrim = putStrLn' . dropWhileEnd (== '\n')

parsePatchelfRpathOutput :: String -> Either ParseError [String]
parsePatchelfRpathOutput =
    parse (many (satisfy (/= colon)) `sepBy` char colon) "patchelf_rpath"
    where colon = ':'

parseLddOutput :: String -> Either ParseError [LddRec]
parseLddOutput = flip parse "ldd" $ manyLines $
    try (toLddRec <$>
             anyCharsTill arrow <*>
                 ((Just .) . (:) <$> char '/' <*> right
                  <|> Nothing <$ string "not found"
                 )
        )
    <|> (`LibOther` Nothing) <$> (try right <|> anyCharsTill (arrow *> addr))
    where manyLines = many . (spaces *>) . (<* newline)
          toLddRec lib | "libHS" `isPrefixOf` lib = LibHS lib
                       | "libffi.so" `isPrefixOf` lib = LibFFI lib
                       | otherwise = LibOther lib
          anyCharsTill = manyTill $ satisfy (/= '\n')
          right = anyCharsTill $ spaces1 *> addr
          addr = string "(0x" *> many1 hexDigit *> char ')'
          arrow = spaces1 *> string "=>" *> spaces1
          spaces1 = skipMany1 space

cmdPlan :: PlanData -> IO ()
cmdPlan PlanData {..} = do
    let buildDir | null planDataBuilddir = ProjectRelativeToDir "."
                 | otherwise = InBuildDir planDataBuilddir
    path <- findPlanJson buildDir
    case planDataQuery of
        Nothing ->
            readFile path >>= putStrLn
        Just (PlanDataQueryDeps proj) -> do
            units <- pjUnits <$> decodePlanJson path
            let comps = [ uComps
                        | Unit {..} <- M.elems units
                        , uType == UnitTypeLocal
                        , let uPkgName = (\(PkgId (PkgName pkg) _) -> pkg) uPId
                        , uPkgName == T.pack proj
                        ]
            when (null comps) $ do
                hPutStrLn stderr $ "Failed to find plan for " ++ proj
                exitFailure
            let deps = foldl (\a curComps ->
                                  let libComps = M.filterWithKey
                                          (const . (== CompNameLib)) curComps
                                  in M.foldr S.union a $
                                      M.map ciLibDeps libComps
                             ) S.empty comps
            forM_ (S.toList deps) $ \(UnitId unit) ->
                putStrLn $ "package-id " ++ T.unpack unit
        Just PlanDataQueryAbi -> do
            PlanJson {..} <- decodePlanJson path
            T.putStrLn $
                T.intercalate "-" [pjArch, pjOs, dispPkgId pjCompilerId]

cmdInit :: InitData -> IO ()
cmdInit init'@InitData {..} = do
    let files = [("cabal.project", cabalProject init', True)
                ,("Setup.hs", setupHs init', True)
                ,(initDataProject ++ ".cabal", projectCabal init', True)
                ,("Makefile", makefile init', True)
                ,(replaceChar '-' '_' initDataProject ++ ".hs"
                 ,projectHs init'
                 ,False
                 )
                ,("hie.yaml", hieYaml init', True)
                ]
    forM_ files $ \(name, file, overridable) ->
        if initDataToStdout
            then printHeader (name ++ "\n") >> T.putStrLn file
            else do
                exists <- doesFileExist name
                if exists
                    then if initDataForce && overridable
                             then T.writeFile name file
                             else hPutStrLn stderr $
                                 if overridable
                                     then useForceMsg name
                                     else existsMsg name
                    else T.writeFile name file
    where printHeader header = do
              isANSITerm <- hSupportsANSI stdout
              if isANSITerm
                  then do
                      setSGR [SetColor Foreground Dull Blue
                             ,SetUnderlining SingleUnderline
                             ]
                      putStrLn header
                      setSGR [Reset]
                  else putStrLn $ " ~~~ " ++ header
          existsMsg name = "File " ++ name ++ " exists"
          useForceMsg name = existsMsg name ++ ", use option -f to override it"

cabalProject :: InitData -> Text
cabalProject InitData {..} = T.concat
    ["packages: ", T.pack initDataProject, ".cabal\n"]

setupHs :: InitData -> Text
setupHs = const
    "import NgxExport.Distribution\n\
    \main = defaultMain\n"

projectCabal :: InitData -> Text
projectCabal InitData {..} = T.concat
    ["name:                       ", T.pack initDataProject, "\n\
     \version:                    0.1.0.0\n\
     \build-type:                 Custom\n\
     \cabal-version:              2.0\n\
     \\n\
     \custom-setup\n\
     \  setup-depends:            base >= 4.8 && < 5\n\
     \                          , ngx-export-distribution\n\
     \\n\
     \library\n\
     \  default-language:         Haskell2010\n\
     \  build-tool-depends:       ngx-export-distribution:nhm-tool\n\
     \  build-depends:            base >= 4.8 && < 5\n\
     \                          , ngx-export\n\
     \\n\
     \  ghc-options:             ", T.pack $ unwords initDataGhcOptions, "\n"
    ,if initDataNoThreaded
         then ""
         else
     "\n\
     \  if impl(ghc >= 9.0.1)\n\
     \    ghc-options:           -threaded\n"
    ]

makefile :: InitData -> Text
makefile InitData {..} = T.concat
    ["NAME := ", T.pack $ replaceChar '-' '_' initDataProject, "\n\
     \PKGNAME := $(subst _,-,$(NAME))\n\
     \PKGVER := 0.1.0.0\n\
     \\n\
     \PREFIX := ", T.pack initDataPrefix, "\n\
     \MACHINE := $(shell uname -m)\n\
     \KERNEL := $(shell uname -s | tr A-Z a-z)\n\
     \\n\
     \PKGDISTR := ngx-export-distribution\n\
     \NHMTOOL := nhm-tool\n\
     \\n\
     \SRC := $(NAME).hs\n\
     \LIB := $(NAME).so\n\
     \DISTR := $(PKGNAME)-$(PKGVER).tar.gz\n\
     \\n\
     \override OBJS := $(SRC:.hs=.o)\n\
     \override HIOBJS := $(SRC:.hs=.hi)\n\
     \override DYNOBJS := $(SRC:.hs=.dyn_o)\n\
     \override DYNHIOBJS := $(SRC:.hs=.dyn_hi)\n\
     \override STUBS := $(SRC:.hs=_stub.h)\n\
     \\n\
     \GHC := ghc\n\
     \CABAL := cabal\n\
     \GHCVER := $(shell $(GHC) --numeric-version)\n\
     \CABALVER := $(shell $(CABAL) --numeric-version)\n\
     \GHCPUID := $(shell \\\n\
     \    if printf '3.12\\n$(CABALVER)' | sort -VC && \\\n\
     \       printf '9.8\\n$(GHCVER)' | sort -VC; \\\n\
     \    then ghcpuid=`$(GHC) --info | sed -n \\\n\
     \             's/^.*\\x28\"Project Unit Id\",\"\\(.*\\)\"\\x29.*$$/\
     \\\1/ip'`; \\\n\
     \        if test -z \"$$ghcpuid\"; \\\n\
     \        then echo \"ghc-$(GHCVER)\"; \\\n\
     \        else echo \"$$ghcpuid\"; \\\n\
     \        fi; \\\n\
     \    else echo \"ghc-$(GHCVER)\"; \\\n\
     \    fi)\n\
     \GHCENV := .ghc.environment.$(MACHINE)-$(KERNEL)-$(GHCVER)\n\
     \GHCENVLNK := ", ghcEnvLnk, "\n\
     \DEPLIBS := $(MACHINE)-$(KERNEL)-$(GHCPUID)\n\
     \BUILDDIR := dist-nhm\n\
     \SETUPCONFIG := $(BUILDDIR)/setup-config\n\
     \\n\
     \.PHONY: all env config install clean clean-all\n\
     \\n\
     \all: $(DISTR)\n\
     \\n\
     \env: $(GHCENVLNK)\n\
     \\n\
     \config: $(SETUPCONFIG)\n\
     \\n\
     \$(GHCENVLNK): cabal.project $(PKGNAME).cabal\n\
     \\trm -f \"$(GHCENVLNK)\"\n\
     \\t$(CABAL) install --builddir=\"$(BUILDDIR)\" --lib --only-dependencies \
     \\\\n\
     \\t  --package-env .\n\
     \\tsed -i 's/\\(^package-id \\)/--\\1/' \"$(GHCENV)\"\n",
     updatePath,
     "\t$(NHMTOOL) deps \"$(PKGNAME)\" -d \"$(BUILDDIR)\" >> \"$(GHCENV)\"\n\
     \\tln -sf \"$(GHCENV)\" \"$(GHCENVLNK)\"\n\
     \\n\
     \$(SETUPCONFIG): $(GHCENVLNK)\n",
     updatePath,
     "\trunhaskell --ghc-arg=-package=base \\\n\
     \\t  --ghc-arg=-package=\"$(PKGDISTR)\" Setup.hs configure \\\n\
     \\t  --builddir=\"$(BUILDDIR)\" \\\n\
     \\t  --package-db=clear --package-db=global \\\n\
     \\t  $$(sed -n 's/^\\(package-db\\)\\s\\+/--\\1=/p' \"$(GHCENV)\") \\\n\
     \\t  $$(sed -n 's/^package-id\\s\\+\\(.*\\)'` \\\n\
     \\t    `'\\(-\\([0-9]\\+\\.\\)*[0-9]\\+\\($$\\|-.*\\)\\)/'` \\\n\
     \\t    `'--dependency=\\1=\\1\\2/p' \\\n\
     \\t    \"$(GHCENV)\") \\\n\
     \\t  --prefix=\"$(PREFIX)\"\n\
     \\n\
     \$(DISTR): $(SETUPCONFIG) $(SRC)\n",
     updatePath,
     "\trunhaskell --ghc-arg=-package=base \\\n\
     \\t  --ghc-arg=-package=\"$(PKGDISTR)\" Setup.hs build \\\n\
     \\t  --builddir=\"$(BUILDDIR)\" \\\n\
     \\t  --ghc-options=\"$(SRC) -o $(LIB) -package-env $(GHCENVLNK) \
     \$(LINKRTS)\"\n\
     \\n\
     \install: $(DISTR)\n\
     \\tinstall -d \"$(PREFIX)\"\n\
     \\ttar xf \"$(DISTR)\" -C \"$(PREFIX)\" --no-same-owner\n\
     \\n\
     \clean:\n\
     \\trm -rf \"$(DEPLIBS)\"\n\
     \\trm -f $(OBJS) $(HIOBJS) $(DYNOBJS) $(DYNHIOBJS) $(STUBS)\n\
     \\trm -f \"$(LIB)\"\n\
     \\n\
     \clean-all: clean\n\
     \\trm -rf \"$(BUILDDIR)\"\n\
     \\trm -f \"$(GHCENV)\" \"$(GHCENVLNK)\" \"$(DISTR)\"\n"
    ]
    where updatePath =
              "\tif test \"$(NHMTOOL)\" = nhm-tool && ! command -v nhm-tool \
              \>/dev/null; \\\n\
              \\tthen \\\n\
              \\t  PATH=$$(dirname $$($(CABAL) list-bin \"$(PKGDISTR)\" \\\n\
              \\t    --builddir=\"$(BUILDDIR)\")):$$PATH; \\\n\
              \\tfi; \\\n"

projectHs :: InitData -> Text
projectHs InitData {..} = T.concat
    ["{-# LANGUAGE TemplateHaskell #-}\n\n\
     \module "
    ,T.pack $ foldr (\v -> if v == '-' || v == '_'
                               then maybe [] (uncurry $ (:) . toUpper) . uncons
                               else (v :)
                    ) "" $ '_' : initDataProject
    ," where\n\n"
    ]

hieYaml :: InitData -> Text
hieYaml InitData {..} = T.concat
    ["cradle:\n\
     \  bios:\n\
     \    shell: |\n\
     \      if [ -L ", ghcEnvLnk, " ]\n\
     \      then\n\
     \          printf \""
    ,T.replace "-" "\\055" $ T.pack $ intercalate "\\n" initDataGhcOptions
    ,"\\n\" > \"$HIE_BIOS_OUTPUT\"\n\
     \          find . -name '[A-Z]*.hs' -exec sh -c '\n\
     \              file=$(echo \"$1\" | cut -c 3-)\n\
     \              if grep -q \"^\\s*module $(echo \"$file\" |\n\
     \                  sed '\\''s/^\\(.*\\)\\..*/\\1/;s/\\//./g'\\'')\" \
     \\"$file\"\n\
     \              then\n\
     \                  echo \"$file\" >> \"$HIE_BIOS_OUTPUT\"\n\
     \              fi\n\
     \          ' sh {} \\;\n\
     \      else\n\
     \          echo \"Ghc environment file wasn't found, \"`\n\
     \              `\"run \\\"make env\\\" and restart \
     \language server.\" >&2\n\
     \          exit 1\n\
     \      fi\n"
    ]

ghcEnvLnk :: Text
ghcEnvLnk = ".ghc.environment.lnk"