1-----------------------------------------------------------------------------
4-- Copyright : (c) 2013-15 Phil Freeman, (c) 2014-15 Gary Burgess
5-- License : MIT (http://opensource.org/licenses/MIT)
7-- Maintainer : Phil Freeman <paf31@cantab.net>
8-- Stability : experimental
13-----------------------------------------------------------------------------
15{-# LANGUAGE DataKinds #-}
16{-# LANGUAGE FlexibleContexts #-}
17{-# LANGUAGE GeneralizedNewtypeDeriving #-}
18{-# LANGUAGE TupleSections #-}
19{-# LANGUAGE RankNTypes #-}
20{-# LANGUAGE RecordWildCards #-}
21{-# LANGUAGE ScopedTypeVariables #-}
22{-# LANGUAGE MultiParamTypeClasses #-}
23{-# LANGUAGE TypeFamilies #-}
25module Language.PureScript.Make
29 , ProgressMessage(..), renderProgressMessage
34 -- * Implementation of Make API using files on disk
43import Control.Monad hiding (sequence)
44import Control.Monad.Error.Class (MonadError(..))
45import Control.Monad.Writer.Class (MonadWriter(..))
46import Control.Monad.Trans.Class (MonadTrans(..))
47import Control.Monad.Trans.Except
48import Control.Monad.IO.Class
49import Control.Monad.Reader (MonadReader(..), ReaderT(..))
50import Control.Monad.Logger
51import Control.Monad.Supply
52import Control.Monad.Base (MonadBase(..))
53import Control.Monad.Trans.Control (MonadBaseControl(..))
55import Control.Concurrent.Lifted as C
57import Data.List (foldl', sort)
58import Data.Maybe (fromMaybe, catMaybes)
60import Data.String (fromString)
61import Data.Foldable (for_)
62import Data.Traversable (for)
63import Data.Version (showVersion)
64import Data.Aeson (encode, decode)
65import qualified Data.ByteString.Lazy as B
66import qualified Data.Set as S
67import qualified Data.Map as M
69import System.Directory
70 (doesFileExist, getModificationTime, createDirectoryIfMissing)
71import System.FilePath ((</>), takeDirectory)
72import System.IO.Error (tryIOError)
74import Language.PureScript.Crash
75import Language.PureScript.AST
76import Language.PureScript.Externs
77import Language.PureScript.Environment
78import Language.PureScript.Errors
79import Language.PureScript.Linter
80import Language.PureScript.ModuleDependencies
81import Language.PureScript.Names
82import Language.PureScript.Options
83import Language.PureScript.Pretty
84import Language.PureScript.Renamer
85import Language.PureScript.Sugar
86import Language.PureScript.TypeChecker
87import qualified Language.PureScript.Constants as C
89import qualified Language.PureScript.CodeGen.JS as J
90import qualified Language.PureScript.CoreFn as CF
91import qualified Paths_purescript as Paths
93-- | Progress messages from the make process
95 = CompilingModule ModuleName
96 deriving (Show, Read, Eq, Ord)
98-- | Render a progress message
99renderProgressMessage :: ProgressMessage -> String
100renderProgressMessage (CompilingModule mn) = "Compiling " ++ runModuleName mn
102-- | Actions that require implementations when running in "make" mode.
104-- This type exists to make two things abstract:
106-- * The particular backend being used (Javascript, C++11, etc.)
108-- * The details of how files are read/written etc.
110data MakeActions m = MakeActions {
112 -- Get the timestamp for the input file(s) for a module. If there are multiple
113 -- files (.purs and foreign files, for example) the timestamp should be for
114 -- the most recently modified file.
116 getInputTimestamp :: ModuleName -> m (Either RebuildPolicy (Maybe UTCTime))
118 -- Get the timestamp for the output files for a module. This should be the
119 -- timestamp for the oldest modified file, or Nothing if any of the required
120 -- output files are missing.
122 , getOutputTimestamp :: ModuleName -> m (Maybe UTCTime)
124 -- Read the externs file for a module as a string and also return the actual
125 -- path for the file.
126 , readExterns :: ModuleName -> m (FilePath, B.ByteString)
128 -- Run the code generator for the module and write any required output files.
130 , codegen :: CF.Module CF.Ann -> Environment -> Externs -> SupplyT m ()
132 -- Respond to a progress update.
134 , progress :: ProgressMessage -> m ()
138-- Generated code for an externs file.
140type Externs = B.ByteString
143-- Determines when to rebuild a module
146 -- | Never rebuild this module
148 -- | Always rebuild this module
149 | RebuildAlways deriving (Show, Read, Eq, Ord)
152-- Compiles in "make" mode, compiling each module separately to a js files and an externs file
154-- If timestamps have not changed, the externs file can be used to provide the module's types without
155-- having to typecheck the module again.
157make :: forall m. (Functor m, Applicative m, Monad m, MonadBaseControl IO m, MonadReader Options m, MonadError MultipleErrors m, MonadWriter MultipleErrors m)
161make MakeActions{..} ms = do
162 checkModuleNamesAreUnique
164 (sorted, graph) <- sortModules ms
166 barriers <- zip (map getModuleName sorted) <$> replicateM (length ms) ((,) <$> C.newEmptyMVar <*> C.newEmptyMVar)
168 for_ sorted $ \m -> fork $ do
169 let deps = fromMaybe (internalError "make: module not found in dependency graph.") (lookup (getModuleName m) graph)
170 buildModule barriers (importPrim m) (deps `inOrderOf` map getModuleName sorted)
172 -- Wait for all threads to complete, and collect errors.
173 errors <- catMaybes <$> for barriers (takeMVar . snd . snd)
175 -- All threads have completed, rethrow any caught errors.
176 unless (null errors) $ throwError (mconcat errors)
178 -- Bundle up all the externs and return them as an Environment
179 (_, externs) <- unzip . fromMaybe (internalError "make: externs were missing but no errors reported.") . sequence <$> for barriers (takeMVar . fst . snd)
180 return $ foldl' (flip applyExternsFileToEnvironment) initEnvironment externs
183 checkModuleNamesAreUnique :: m ()
184 checkModuleNamesAreUnique =
185 case findDuplicate (map getModuleName ms) of
187 Just mn -> throwError . errorMessage $ DuplicateModuleName mn
189 -- Verify that a list of values has unique keys
190 findDuplicate :: (Ord a) => [a] -> Maybe a
191 findDuplicate = go . sort
195 | otherwise = go (y : xs)
198 -- Sort a list so its elements appear in the same order as in another list.
199 inOrderOf :: (Ord a) => [a] -> [a] -> [a]
200 inOrderOf xs ys = let s = S.fromList xs in filter (`S.member` s) ys
202 buildModule :: [(ModuleName, (C.MVar (Maybe (MultipleErrors, ExternsFile)), C.MVar (Maybe MultipleErrors)))] -> Module -> [ModuleName] -> m ()
203 buildModule barriers m@(Module _ _ moduleName _ _) deps = flip catchError (markComplete Nothing . Just) $ do
204 -- We need to wait for dependencies to be built, before checking if the current
205 -- module should be rebuilt, so the first thing to do is to wait on the
206 -- MVars for the module's dependencies.
207 mexterns <- fmap unzip . sequence <$> traverse (readMVar . fst . fromMaybe (internalError "make: no barrier") . flip lookup barriers) deps
210 Just (_, externs) -> do
211 outputTimestamp <- getOutputTimestamp moduleName
212 dependencyTimestamp <- maximumMaybe <$> traverse (fmap shouldExist . getOutputTimestamp) deps
213 inputTimestamp <- getInputTimestamp moduleName
215 let shouldRebuild = case (inputTimestamp, dependencyTimestamp, outputTimestamp) of
216 (Right (Just t1), Just t3, Just t2) -> t1 > t2 || t3 > t2
217 (Right (Just t1), Nothing, Just t2) -> t1 > t2
218 (Left RebuildNever, _, Just _) -> False
222 (exts, warnings) <- listen $ do
223 progress $ CompilingModule moduleName
224 let env = foldl' (flip applyExternsFileToEnvironment) initEnvironment externs
226 ([desugared], nextVar) <- runSupplyT 0 $ desugar externs [m]
227 (checked@(Module ss coms _ elaborated exps), env') <- runCheck' env $ typeCheckModule desugared
228 checkExhaustiveModule env' checked
229 regrouped <- createBindingGroups moduleName . collapseBindingGroups $ elaborated
230 let mod' = Module ss coms moduleName regrouped exps
231 corefn = CF.moduleToCoreFn env' mod'
232 [renamed] = renameInModules [corefn]
233 exts = moduleToExternsFile mod' env'
234 evalSupplyT nextVar $ codegen renamed env' $ encode exts
236 markComplete (Just (warnings, exts)) Nothing
241 mexts <- decodeExterns . snd <$> readExterns moduleName
243 Just exts -> markComplete (Just (mempty, exts)) Nothing
245 Nothing -> markComplete Nothing Nothing
247 markComplete :: Maybe (MultipleErrors, ExternsFile) -> Maybe MultipleErrors -> m ()
248 markComplete externs errors = do
249 putMVar (fst $ fromMaybe (internalError "make: no barrier") $ lookup moduleName barriers) externs
250 putMVar (snd $ fromMaybe (internalError "make: no barrier") $ lookup moduleName barriers) errors
252 maximumMaybe :: (Ord a) => [a] -> Maybe a
253 maximumMaybe [] = Nothing
254 maximumMaybe xs = Just $ maximum xs
256 -- Make sure a dependency exists
257 shouldExist :: Maybe UTCTime -> UTCTime
258 shouldExist (Just t) = t
259 shouldExist _ = internalError "make: dependency should already have been built."
261 decodeExterns :: B.ByteString -> Maybe ExternsFile
262 decodeExterns bs = do
264 guard $ efVersion externs == showVersion Paths.version
268-- Add an import declaration for a module if it does not already explicitly import it.
270addDefaultImport :: ModuleName -> Module -> Module
271addDefaultImport toImport m@(Module ss coms mn decls exps) =
272 if isExistingImport `any` decls || mn == toImport then m
273 else Module ss coms mn (ImportDeclaration toImport Implicit Nothing : decls) exps
275 isExistingImport (ImportDeclaration mn' _ _) | mn' == toImport = True
276 isExistingImport (PositionedDeclaration _ _ d) = isExistingImport d
277 isExistingImport _ = False
279importPrim :: Module -> Module
280importPrim = addDefaultImport (ModuleName [ProperName C.prim])
283-- A monad for running make actions
285newtype Make a = Make { unMake :: ReaderT Options (ExceptT MultipleErrors (Logger MultipleErrors)) a }
286 deriving (Functor, Applicative, Monad, MonadIO, MonadError MultipleErrors, MonadWriter MultipleErrors, MonadReader Options)
288instance MonadBase IO Make where
291instance MonadBaseControl IO Make where
292 type StM Make a = Either MultipleErrors a
293 liftBaseWith f = Make $ liftBaseWith $ \q -> f (q . unMake)
294 restoreM = Make . restoreM
297-- Execute a 'Make' monad, returning either errors, or the result of the compile plus any warnings.
299runMake :: Options -> Make a -> IO (Either MultipleErrors a, MultipleErrors)
300runMake opts = runLogger' . runExceptT . flip runReaderT opts . unMake
302makeIO :: (IOError -> ErrorMessage) -> IO a -> Make a
304 e <- liftIO $ tryIOError io
305 either (throwError . singleError . f) return e
307-- Traverse (Either e) instance (base 4.7)
308traverseEither :: Applicative f => (a -> f b) -> Either e a -> f (Either e b)
309traverseEither _ (Left x) = pure (Left x)
310traverseEither f (Right y) = Right <$> f y
313-- A set of make actions that read and write modules from the given directory.
315buildMakeActions :: FilePath -- ^ the output directory
316 -> M.Map ModuleName (Either RebuildPolicy FilePath) -- ^ a map between module names and paths to the file containing the PureScript module
317 -> M.Map ModuleName FilePath -- ^ a map between module name and the file containing the foreign javascript for the module
318 -> Bool -- ^ Generate a prefix comment?
320buildMakeActions outputDir filePathMap foreigns usePrefix =
321 MakeActions getInputTimestamp getOutputTimestamp readExterns codegen progress
324 getInputTimestamp :: ModuleName -> Make (Either RebuildPolicy (Maybe UTCTime))
325 getInputTimestamp mn = do
326 let path = fromMaybe (internalError "Module has no filename in 'make'") $ M.lookup mn filePathMap
327 e1 <- traverseEither getTimestamp path
328 fPath <- maybe (return Nothing) getTimestamp $ M.lookup mn foreigns
329 return $ fmap (max fPath) e1
331 getOutputTimestamp :: ModuleName -> Make (Maybe UTCTime)
332 getOutputTimestamp mn = do
333 let filePath = runModuleName mn
334 jsFile = outputDir </> filePath </> "index.js"
335 externsFile = outputDir </> filePath </> "externs.json"
336 min <$> getTimestamp jsFile <*> getTimestamp externsFile
338 readExterns :: ModuleName -> Make (FilePath, B.ByteString)
340 let path = outputDir </> runModuleName mn </> "externs.json"
341 (path, ) <$> readTextFile path
343 codegen :: CF.Module CF.Ann -> Environment -> Externs -> SupplyT Make ()
344 codegen m _ exts = do
345 let mn = CF.moduleName m
346 foreignInclude <- case mn `M.lookup` foreigns of
348 | not $ requiresForeign m -> do
349 tell $ errorMessage $ UnnecessaryFFIModule mn path
351 | otherwise -> return $ Just $ J.JSApp (J.JSVar "require") [J.JSStringLiteral "./foreign"]
352 Nothing | requiresForeign m -> throwError . errorMessage $ MissingFFIModule mn
353 | otherwise -> return Nothing
354 pjs <- prettyPrintJS <$> J.moduleToJs m foreignInclude
355 let filePath = runModuleName mn
356 jsFile = outputDir </> filePath </> "index.js"
357 externsFile = outputDir </> filePath </> "externs.json"
358 foreignFile = outputDir </> filePath </> "foreign.js"
359 prefix = ["Generated by psc version " ++ showVersion Paths.version | usePrefix]
360 js = unlines $ map ("// " ++) prefix ++ [pjs]
362 writeTextFile jsFile (fromString js)
363 for_ (mn `M.lookup` foreigns) (readTextFile >=> writeTextFile foreignFile)
364 writeTextFile externsFile exts
366 requiresForeign :: CF.Module a -> Bool
367 requiresForeign = not . null . CF.moduleForeign
369 getTimestamp :: FilePath -> Make (Maybe UTCTime)
370 getTimestamp path = makeIO (const (ErrorMessage [] $ CannotGetFileInfo path)) $ do
371 exists <- doesFileExist path
372 traverse (const $ getModificationTime path) $ guard exists
374 readTextFile :: FilePath -> Make B.ByteString
375 readTextFile path = makeIO (const (ErrorMessage [] $ CannotReadFile path)) $ B.readFile path
377 writeTextFile :: FilePath -> B.ByteString -> Make ()
378 writeTextFile path text = makeIO (const (ErrorMessage [] $ CannotWriteFile path)) $ do
380 B.writeFile path text
382 mkdirp :: FilePath -> IO ()
383 mkdirp = createDirectoryIfMissing True . takeDirectory
385 progress :: ProgressMessage -> Make ()
386 progress = liftIO . putStrLn . renderProgressMessage
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list