{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Language.Ninja.Compile
( compile
) where
import Control.Applicative ((<|>))
import Control.Arrow (first)
import qualified Control.Lens as Lens
import Control.Monad.Error.Class (MonadError)
import Data.Char (isSpace)
import Data.Maybe (fromMaybe, isJust)
import Data.Monoid (Endo (Endo, appEndo), (<>))
import Data.Text (Text)
import qualified Data.Text as Text
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import Data.HashSet (HashSet)
import qualified Data.HashSet as HS
import Flow ((.>), (|>))
import Data.Hashable (Hashable)
import qualified Data.Versions as Ver
import qualified Language.Ninja.AST as AST
import qualified Language.Ninja.Errors as Errors
import qualified Language.Ninja.IR as IR
import qualified Language.Ninja.Misc as Misc
compile :: forall m ann. (MonadError Errors.CompileError m)
=> AST.Ninja ann -> m IR.Ninja
compile ast = result
where
result :: m IR.Ninja
result = do
meta <- metaM
builds <- buildsM
phonys <- phonysM
defaults <- defaultsM
pools <- poolsM
IR.makeNinja
|> Lens.set IR.ninjaMeta meta
|> Lens.set IR.ninjaBuilds builds
|> Lens.set IR.ninjaPhonys phonys
|> Lens.set IR.ninjaDefaults defaults
|> Lens.set IR.ninjaPools pools
|> pure
metaM :: m IR.Meta
metaM = do
let getSpecial :: Text -> Maybe Text
getSpecial name = HM.lookup name (Lens.view AST.ninjaSpecials ast)
let parseVersion :: Text -> m Ver.Version
parseVersion = Ver.version
.> either Errors.throwVersionParseFailure pure
reqversion <- getSpecial "ninja_required_version"
|> fmap parseVersion
|> sequenceA
builddir <- getSpecial "builddir"
|> fmap Misc.makePath
|> pure
IR.makeMeta
|> Lens.set IR.metaReqVersion reqversion
|> Lens.set IR.metaBuildDir builddir
|> pure
buildsM :: m (HashSet IR.Build)
buildsM = (multiplesAST <> onHM (first HS.singleton) singlesAST)
|> HM.toList |> mapM compileBuild |> fmap HS.fromList
phonysM :: m (HashMap IR.Target (HashSet IR.Target))
phonysM = HM.toList phonysAST |> mapM compilePhony |> fmap HM.fromList
defaultsM :: m (HashSet IR.Target)
defaultsM = HS.toList defaultsAST |> mapM compileDefault |> fmap HS.fromList
poolsM :: m (HashSet IR.Pool)
poolsM = HM.toList poolsAST |> mapM compilePool |> fmap HS.fromList
compileBuild :: (HashSet Text, AST.Build ann) -> m IR.Build
compileBuild (outputs, buildAST) = do
let depsAST = Lens.view AST.buildDeps buildAST
let normalDeps = HS.toList (Lens.view AST.depsNormal depsAST)
let implicitDeps = HS.toList (Lens.view AST.depsImplicit depsAST)
let orderOnlyDeps = HS.toList (Lens.view AST.depsOrderOnly depsAST)
outs <- HS.toList outputs |> mapM compileOutput |> fmap HS.fromList
rule <- compileRule (outs, buildAST)
deps <- let compileDep ty dep = compileDependency (dep, ty)
in (\n i o -> HS.fromList (n <> i <> o))
<$> mapM (compileDep IR.NormalDependency) normalDeps
<*> mapM (compileDep IR.NormalDependency) implicitDeps
<*> mapM (compileDep IR.OrderOnlyDependency) orderOnlyDeps
IR.makeBuild rule
|> Lens.set IR.buildOuts outs
|> Lens.set IR.buildDeps deps
|> pure
compilePhony :: (Text, HashSet Text)
-> m (IR.Target, HashSet IR.Target)
compilePhony (name, deps) = do
ename <- compileTarget name
edeps <- HS.fromList <$> mapM compileTarget (HS.toList deps)
pure (ename, edeps)
compileDefault :: Text -> m IR.Target
compileDefault = compileTarget
compilePool :: (Text, Int) -> m IR.Pool
compilePool pair = case pair of
("console", 1) -> pure IR.makePoolConsole
("console", d) -> Errors.throwInvalidPoolDepth d
("", _) -> Errors.throwEmptyPoolName
(name, d) -> do dp <- Misc.makePositive d
|> maybe (Errors.throwInvalidPoolDepth d) pure
pure (IR.makePoolCustom name dp)
compileRule :: (HashSet IR.Output, AST.Build ann) -> m IR.Rule
compileRule (outputs, buildAST) = do
(name, ruleAST) <- lookupRule buildAST
let orLookupError :: Text -> Maybe a -> m a
orLookupError var = maybe (Errors.throwRuleLookupFailure var) pure
let env = computeRuleEnv (outputs, buildAST) ruleAST
let lookupBind :: Text -> m (Maybe Text)
lookupBind = AST.askEnv env .> pure
let lookupBind_ :: Text -> m Text
lookupBind_ var = lookupBind var >>= orLookupError var
command <- lookupBind_ "command" >>= compileCommand
description <- lookupBind "description"
pool <- let buildBind = Lens.view AST.buildBind buildAST
in (HM.lookup "pool" buildBind <|> AST.askEnv env "pool")
|> fromMaybe ""
|> IR.parsePoolName
|> pure
depfile <- lookupBind "depfile"
|> fmap (fmap Misc.makePath)
specialDeps <- let lookupPrefix = lookupBind "msvc_deps_prefix"
in ((,) <$> lookupBind "deps" <*> lookupPrefix)
>>= compileSpecialDeps
generator <- isJust <$> lookupBind "generator"
restat <- isJust <$> lookupBind "restat"
responseFile <- do ma <- lookupBind "rspfile"
mb <- lookupBind "rspfile_content"
pure ((,) <$> ma <*> mb)
>>= fmap compileResponseFile .> sequenceA
IR.makeRule name command
|> Lens.set IR.ruleDescription description
|> Lens.set IR.rulePool pool
|> Lens.set IR.ruleDepfile depfile
|> Lens.set IR.ruleSpecialDeps specialDeps
|> Lens.set IR.ruleGenerator generator
|> Lens.set IR.ruleRestat restat
|> Lens.set IR.ruleResponseFile responseFile
|> pure
compileSpecialDeps :: (Maybe Text, Maybe Text) -> m (Maybe IR.SpecialDeps)
compileSpecialDeps = (\case (Nothing, _) -> pure Nothing
(Just "gcc", m) -> goGCC m
(Just "msvc", m) -> goMSVC m
(Just d, _) -> Errors.throwUnknownDeps d)
where
goGCC, goMSVC :: Maybe Text -> m (Maybe IR.SpecialDeps)
goGCC (Just _) = Errors.throwUnexpectedMSVCPrefix "gcc"
goGCC Nothing = pure (Just IR.makeSpecialDepsGCC)
goMSVC (Just m) = pure (Just (IR.makeSpecialDepsMSVC m))
goMSVC Nothing = pure (Just (IR.makeSpecialDepsMSVC defaultPrefix))
defaultPrefix = "Note: including file: " :: Text
compileResponseFile :: (Text, Text) -> m IR.ResponseFile
compileResponseFile (file, content) = do
let path = Misc.makePath file
pure (IR.makeResponseFile path content)
compileTarget :: Text -> m IR.Target
compileTarget = IR.makeTarget .> pure
compileOutput :: Text -> m IR.Output
compileOutput name = do
target <- compileTarget name
pure (IR.makeOutput target)
compileDependency :: (Text, IR.DependencyType) -> m IR.Dependency
compileDependency (name, ty) = do
target <- compileTarget name
pure (IR.makeDependency target ty)
compileCommand :: Text -> m Misc.Command
compileCommand = Misc.makeCommand .> pure
lookupRule :: AST.Build ann -> m (Text, AST.Rule ann)
lookupRule buildAST = do
let name = Lens.view AST.buildRule buildAST
ruleAST <- HM.lookup name rulesAST
|> maybe (Errors.throwBuildRuleNotFound name) pure
pure (name, ruleAST)
computeRuleEnv :: (HashSet IR.Output, AST.Build ann)
-> AST.Rule ann
-> AST.Env Text Text
computeRuleEnv (outs, buildAST) ruleAST = do
let depsAST = Lens.view AST.buildDeps buildAST
let isExplicitOut :: IR.Output -> Bool
isExplicitOut _ = True
let explicitOuts = HS.toList outs
|> filter isExplicitOut
|> map (Lens.view (IR.outputTarget . IR.targetText))
let explicitDeps = [ Lens.view AST.depsNormal depsAST
, Lens.view AST.depsOrderOnly depsAST
] |> mconcat |> HS.toList
let composeList :: [a -> a] -> (a -> a)
composeList = map Endo .> mconcat .> appEndo
let quote :: Text -> Text
quote x | Text.any isSpace x = mconcat ["\"", x, "\""]
quote x = x
let bindings = Lens.view AST.buildBind buildAST
AST.scopeEnv (Lens.view AST.buildEnv buildAST)
|> AST.addEnv "out" (Text.unwords (map quote explicitOuts))
|> AST.addEnv "in" (Text.unwords (map quote explicitDeps))
|> AST.addEnv "in_newline" (Text.unlines explicitDeps)
|> composeList (map (uncurry AST.addEnv) (HM.toList bindings))
|> AST.addBinds (HM.toList (Lens.view AST.ruleBind ruleAST))
rulesAST :: HashMap Text (AST.Rule ann)
singlesAST :: HashMap Text (AST.Build ann)
multiplesAST :: HashMap (HashSet Text) (AST.Build ann)
phonysAST :: HashMap Text (HashSet Text)
defaultsAST :: HashSet Text
poolsAST :: HashMap Text Int
rulesAST = Lens.view AST.ninjaRules ast
singlesAST = Lens.view AST.ninjaSingles ast
multiplesAST = Lens.view AST.ninjaMultiples ast
phonysAST = Lens.view AST.ninjaPhonys ast
defaultsAST = Lens.view AST.ninjaDefaults ast
poolsAST = Lens.view AST.ninjaPools ast
onHM :: (Eq k', Hashable k')
=> ((k, v) -> (k', v')) -> HashMap k v -> HashMap k' v'
onHM f = HM.toList .> map f .> HM.fromList