summaryrefslogtreecommitdiff
path: root/Coolbal/Target.hs
blob: c698ceb0e1234e4d73e905f6971bc986f5af3e19 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
module Coolbal.Target (
    IsTarget(..),
    AnyTarget(..),
    ExeTarget(..),
    useAnyTarget,
) where

import Data.Char (ord)
import Data.List (intercalate)
import Numeric (showHex)
import System.Directory (createDirectoryIfMissing)
import System.Exit (ExitCode(..), die)
import System.FilePath ((</>))
import System.Process (rawSystem)

import Coolbal.Util


class IsTarget a where
    -- | The name of the target.
    targetName :: a -> String

    -- | The name of the target, qualified with a cabal-style prefix indicating the kind of target.
    targetNameQualified :: a -> String

    -- | The name of the target, qualified with a cabal-style prefix indicating
    -- the kind of target, except that the ':' is rendered as a '-'.
    targetNameQualifiedFilename :: a -> FilePath
    targetNameQualifiedFilename tg = case break (== ':') (targetNameQualified tg) of
        (pre, ':' : post) -> pre ++ '-' : post
        _ -> error "targetNameQualifiedFilename: unexpected form of targetNameQualified"

    -- | Check whether the target must be recompiled due to changes on disk.
    -- Argument is the root directory of the project.
    targetCheckOld :: FilePath -> a -> IO Bool

    -- | Recompile the target. Argument is the root directory of the project.
    targetBuild :: FilePath -> a -> IO ()

data AnyTarget = forall a. (Show a, IsTarget a) => AnyTarget a

deriving instance Show AnyTarget

useAnyTarget :: (forall a. (Show a, IsTarget a) => a -> r) -> AnyTarget -> r
useAnyTarget f (AnyTarget x) = f x

data ExeTarget = ExeTarget
    { exeTargetName :: String
      -- ^ Target name, and name of output executable file
    , exeTargetPkgDbDir :: FilePath
      -- ^ Absolute path to the package.db directory
    , exeTargetDeps :: [String]
      -- ^ Package identifiers of dependencies (for -package-id flags)
    , exeTargetLanguage :: String
      -- ^ Haskell language (e.g. Haskell2010)
    , exeTargetMain :: FilePath
      -- ^ Main file
    , exeTargetModules :: [[String]]
      -- ^ Other modules in the target
    , exeTargetFlags :: [String]
      -- ^ User-specified compiler flags
    }
  deriving (Show)

instance IsTarget ExeTarget where
    targetName = exeTargetName
    targetNameQualified e = "exe:" ++ targetName e

    targetCheckOld _ _ = return True

    targetBuild projdir tg = do
        let buildDir = projdir </> "dist-coolbal/build"
                               </> escapeFileName (targetNameQualifiedFilename tg)
            binDir = projdir </> "dist-coolbal/bin"
            binName = binDir </> escapeFileName (exeTargetName tg)
        createDirectoryIfMissing True binDir
        rawSystem "ghc" (concat
            [["--make", "-static"]
            ,concat [[flag, buildDir]
                    | flag <- ["-outputdir", "-odir", "-hidir", "-stubdir"]]
            ,["-i" ++ buildDir, "-I" ++ buildDir]
            ,["-hide-all-packages", "-Wmissing-home-modules", "-no-user-package-db"]
            ,["-package-db", exeTargetPkgDbDir tg]
            ,concat [["-package-id", dep] | dep <- exeTargetDeps tg]
            ,["-X" ++ exeTargetLanguage tg]
            ,[exeTargetMain tg]
            ,map (intercalate ".") (exeTargetModules tg)
            ,["-o", binName]
            ,exeTargetFlags tg])
          >>= checkExitCode "ghc"

checkExitCode :: String -> ExitCode -> IO ()
checkExitCode _ ExitSuccess = return ()
checkExitCode procname (ExitFailure c) =
    die (procname ++ " exited with code " ++ show c)

escapeFileName :: String -> FilePath
escapeFileName =
    -- NTFS/POSIX not allows: 0x00-0x1F 0x7F- : / " * < > ? \ |
    -- We use ! as an escape character, and hence disallow that too.
    genericEscapeString
        (\c -> let n = ord c
               in n <= 0x1F || n >= 0x7F || c `elem` ":/\"*<>?\\|!")
        (\c -> case (c, ord c) of
            ('!', _) -> "!!"
            (_, n) | n <= 0xFF -> '!' : showHex n ""
                   | n <= 0xFFFF -> '!' : 'u' : showHex n ""
                   | n <= 0xFFFFFF -> '!' : 'U' : showHex n ""
                   | otherwise -> error "Super-high unicode?")