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 #-}
26module Language.PureScript.Make
30 , ProgressMessage(..), renderProgressMessage
35 -- * Implementation of Make API using files on disk
41#if __GLASGOW_HASKELL__ < 710
42import Control.Applicative
45import Control.Monad.Error.Class (MonadError(..))
46import Control.Monad.Writer.Class (MonadWriter(..))
47import Control.Monad.Trans.Except
48import Control.Monad.Reader
49import Control.Monad.Logger
50import Control.Monad.Supply
51import Control.Monad.Base (MonadBase(..))
52import Control.Monad.Trans.Control (MonadBaseControl(..))
54import Control.Concurrent.Lifted as C
56import Data.List (foldl', sort)
57import Data.Maybe (fromMaybe, catMaybes)
59import Data.String (fromString)
60import Data.Foldable (for_)
61#if __GLASGOW_HASKELL__ < 710
62import Data.Traversable (traverse)
64import Data.Traversable (for)
65import Data.Version (showVersion)
66import Data.Aeson (encode, decode)
67import qualified Data.ByteString.Lazy as B
68import qualified Data.Set as S
69import qualified Data.Map as M
71import System.Directory
72 (doesFileExist, getModificationTime, createDirectoryIfMissing)
73import System.FilePath ((</>), takeDirectory)
74import System.IO.Error (tryIOError)
76import Language.PureScript.Crash
77import Language.PureScript.AST
78import Language.PureScript.Externs
79import Language.PureScript.Environment
80import Language.PureScript.Errors
81import Language.PureScript.Linter
82import Language.PureScript.ModuleDependencies
83import Language.PureScript.Names
84import Language.PureScript.Options
85import Language.PureScript.Pretty
86import Language.PureScript.Renamer
87import Language.PureScript.Sugar
88import Language.PureScript.TypeChecker
89import qualified Language.PureScript.Constants as C
91import qualified Language.PureScript.CodeGen.JS as J
92import qualified Language.PureScript.CoreFn as CF
93import qualified Paths_purescript as Paths
95-- | Progress messages from the make process
97 = CompilingModule ModuleName
98 deriving (Show, Read, Eq, Ord)
100-- | Render a progress message
101renderProgressMessage :: ProgressMessage -> String
102renderProgressMessage (CompilingModule mn) = "Compiling " ++ runModuleName mn
104-- | Actions that require implementations when running in "make" mode.
106-- This type exists to make two things abstract:
108-- * The particular backend being used (Javascript, C++11, etc.)
110-- * The details of how files are read/written etc.
112data MakeActions m = MakeActions {
114 -- Get the timestamp for the input file(s) for a module. If there are multiple
115 -- files (.purs and foreign files, for example) the timestamp should be for
116 -- the most recently modified file.
118 getInputTimestamp :: ModuleName -> m (Either RebuildPolicy (Maybe UTCTime))
120 -- Get the timestamp for the output files for a module. This should be the
121 -- timestamp for the oldest modified file, or Nothing if any of the required
122 -- output files are missing.
124 , getOutputTimestamp :: ModuleName -> m (Maybe UTCTime)
126 -- Read the externs file for a module as a string and also return the actual
127 -- path for the file.
128 , readExterns :: ModuleName -> m (FilePath, B.ByteString)
130 -- Run the code generator for the module and write any required output files.
132 , codegen :: CF.Module CF.Ann -> Environment -> Externs -> SupplyT m ()
134 -- Respond to a progress update.
136 , progress :: ProgressMessage -> m ()
140-- Generated code for an externs file.
142type Externs = B.ByteString
145-- Determines when to rebuild a module
148 -- | Never rebuild this module
150 -- | Always rebuild this module
151 | RebuildAlways deriving (Show, Read, Eq, Ord)
154-- Compiles in "make" mode, compiling each module separately to a js files and an externs file
156-- If timestamps have not changed, the externs file can be used to provide the module's types without
157-- having to typecheck the module again.
159make :: forall m. (Functor m, Applicative m, Monad m, MonadBaseControl IO m, MonadReader Options m, MonadError MultipleErrors m, MonadWriter MultipleErrors m)
163make MakeActions{..} ms = do
164 checkModuleNamesAreUnique
166 (sorted, graph) <- sortModules ms
168 barriers <- zip (map getModuleName sorted) <$> replicateM (length ms) ((,) <$> C.newEmptyMVar <*> C.newEmptyMVar)
170 for_ sorted $ \m -> fork $ do
171 let deps = fromMaybe (internalError "make: module not found in dependency graph.") (lookup (getModuleName m) graph)
172 buildModule barriers (importPrim m) (deps `inOrderOf` map getModuleName sorted)
174 -- Wait for all threads to complete, and collect errors.
175 errors <- catMaybes <$> for barriers (takeMVar . snd . snd)
177 -- All threads have completed, rethrow any caught errors.
178 unless (null errors) $ throwError (mconcat errors)
180 -- Bundle up all the externs and return them as an Environment
181 (warnings, externs) <- unzip . fromMaybe (internalError "make: externs were missing but no errors reported.") . sequence <$> for barriers (takeMVar . fst . snd)
182 tell (mconcat warnings)
183 return $ foldl' (flip applyExternsFileToEnvironment) initEnvironment externs
186 checkModuleNamesAreUnique :: m ()
187 checkModuleNamesAreUnique =
188 case findDuplicate (map getModuleName ms) of
190 Just mn -> throwError . errorMessage $ DuplicateModuleName mn
192 -- Verify that a list of values has unique keys
193 findDuplicate :: (Ord a) => [a] -> Maybe a
194 findDuplicate = go . sort
198 | otherwise = go (y : xs)
201 -- Sort a list so its elements appear in the same order as in another list.
202 inOrderOf :: (Ord a) => [a] -> [a] -> [a]
203 inOrderOf xs ys = let s = S.fromList xs in filter (`S.member` s) ys
205 buildModule :: [(ModuleName, (C.MVar (Maybe (MultipleErrors, ExternsFile)), C.MVar (Maybe MultipleErrors)))] -> Module -> [ModuleName] -> m ()
206 buildModule barriers m@(Module _ _ moduleName _ _) deps = flip catchError (markComplete Nothing . Just) $ do
207 -- We need to wait for dependencies to be built, before checking if the current
208 -- module should be rebuilt, so the first thing to do is to wait on the
209 -- MVars for the module's dependencies.
210 mexterns <- fmap unzip . sequence <$> mapM (readMVar . fst . fromMaybe (internalError "make: no barrier") . flip lookup barriers) deps
213 Just (_, externs) -> do
214 outputTimestamp <- getOutputTimestamp moduleName
215 dependencyTimestamp <- maximumMaybe <$> mapM (fmap shouldExist . getOutputTimestamp) deps
216 inputTimestamp <- getInputTimestamp moduleName
218 let shouldRebuild = case (inputTimestamp, dependencyTimestamp, outputTimestamp) of
219 (Right (Just t1), Just t3, Just t2) -> t1 > t2 || t3 > t2
220 (Right (Just t1), Nothing, Just t2) -> t1 > t2
221 (Left RebuildNever, _, Just _) -> False
225 (exts, warnings) <- listen $ do
226 progress $ CompilingModule moduleName
227 let env = foldl' (flip applyExternsFileToEnvironment) initEnvironment externs
229 ([desugared], nextVar) <- runSupplyT 0 $ desugar externs [m]
230 (checked@(Module ss coms _ elaborated exps), env') <- runCheck' env $ typeCheckModule desugared
231 checkExhaustiveModule env' checked
232 regrouped <- createBindingGroups moduleName . collapseBindingGroups $ elaborated
233 let mod' = Module ss coms moduleName regrouped exps
234 corefn = CF.moduleToCoreFn env' mod'
235 [renamed] = renameInModules [corefn]
236 exts = moduleToExternsFile mod' env'
237 evalSupplyT nextVar $ codegen renamed env' $ encode exts
239 markComplete (Just (warnings, exts)) Nothing
244 mexts <- decodeExterns . snd <$> readExterns moduleName
246 Just exts -> markComplete (Just (mempty, exts)) Nothing
248 Nothing -> markComplete Nothing Nothing
250 markComplete :: Maybe (MultipleErrors, ExternsFile) -> Maybe MultipleErrors -> m ()
251 markComplete externs errors = do
252 putMVar (fst $ fromMaybe (internalError "make: no barrier") $ lookup moduleName barriers) externs
253 putMVar (snd $ fromMaybe (internalError "make: no barrier") $ lookup moduleName barriers) errors
255 maximumMaybe :: (Ord a) => [a] -> Maybe a
256 maximumMaybe [] = Nothing
257 maximumMaybe xs = Just $ maximum xs
259 -- Make sure a dependency exists
260 shouldExist :: Maybe UTCTime -> UTCTime
261 shouldExist (Just t) = t
262 shouldExist _ = internalError "make: dependency should already have been built."
264 decodeExterns :: B.ByteString -> Maybe ExternsFile
265 decodeExterns bs = do
267 guard $ efVersion externs == showVersion Paths.version
271-- Add an import declaration for a module if it does not already explicitly import it.
273addDefaultImport :: ModuleName -> Module -> Module
274addDefaultImport toImport m@(Module ss coms mn decls exps) =
275 if isExistingImport `any` decls || mn == toImport then m
276 else Module ss coms mn (ImportDeclaration toImport Implicit Nothing : decls) exps
278 isExistingImport (ImportDeclaration mn' _ _) | mn' == toImport = True
279 isExistingImport (PositionedDeclaration _ _ d) = isExistingImport d
280 isExistingImport _ = False
282importPrim :: Module -> Module
283importPrim = addDefaultImport (ModuleName [ProperName C.prim])
286-- A monad for running make actions
288newtype Make a = Make { unMake :: ReaderT Options (ExceptT MultipleErrors (Logger MultipleErrors)) a }
289 deriving (Functor, Applicative, Monad, MonadIO, MonadError MultipleErrors, MonadWriter MultipleErrors, MonadReader Options)
291instance MonadBase IO Make where
294instance MonadBaseControl IO Make where
295 type StM Make a = Either MultipleErrors a
296 liftBaseWith f = Make $ liftBaseWith $ \q -> f (q . unMake)
297 restoreM = Make . restoreM
300-- Execute a 'Make' monad, returning either errors, or the result of the compile plus any warnings.
302runMake :: Options -> Make a -> IO (Either MultipleErrors a, MultipleErrors)
303runMake opts = runLogger' . runExceptT . flip runReaderT opts . unMake
305makeIO :: (IOError -> ErrorMessage) -> IO a -> Make a
307 e <- liftIO $ tryIOError io
308 either (throwError . singleError . f) return e
310-- Traverse (Either e) instance (base 4.7)
311traverseEither :: Applicative f => (a -> f b) -> Either e a -> f (Either e b)
312traverseEither _ (Left x) = pure (Left x)
313traverseEither f (Right y) = Right <$> f y
316-- A set of make actions that read and write modules from the given directory.
318buildMakeActions :: FilePath -- ^ the output directory
319 -> M.Map ModuleName (Either RebuildPolicy FilePath) -- ^ a map between module names and paths to the file containing the PureScript module
320 -> M.Map ModuleName FilePath -- ^ a map between module name and the file containing the foreign javascript for the module
321 -> Bool -- ^ Generate a prefix comment?
323buildMakeActions outputDir filePathMap foreigns usePrefix =
324 MakeActions getInputTimestamp getOutputTimestamp readExterns codegen progress
327 getInputTimestamp :: ModuleName -> Make (Either RebuildPolicy (Maybe UTCTime))
328 getInputTimestamp mn = do
329 let path = fromMaybe (internalError "Module has no filename in 'make'") $ M.lookup mn filePathMap
330 e1 <- traverseEither getTimestamp path
331 fPath <- maybe (return Nothing) getTimestamp $ M.lookup mn foreigns
332 return $ fmap (max fPath) e1
334 getOutputTimestamp :: ModuleName -> Make (Maybe UTCTime)
335 getOutputTimestamp mn = do
336 let filePath = runModuleName mn
337 jsFile = outputDir </> filePath </> "index.js"
338 externsFile = outputDir </> filePath </> "externs.json"
339 min <$> getTimestamp jsFile <*> getTimestamp externsFile
341 readExterns :: ModuleName -> Make (FilePath, B.ByteString)
343 let path = outputDir </> runModuleName mn </> "externs.json"
344 (path, ) <$> readTextFile path
346 codegen :: CF.Module CF.Ann -> Environment -> Externs -> SupplyT Make ()
347 codegen m _ exts = do
348 let mn = CF.moduleName m
349 foreignInclude <- case mn `M.lookup` foreigns of
351 | not $ requiresForeign m -> do
352 tell $ errorMessage $ UnnecessaryFFIModule mn path
354 | otherwise -> return $ Just $ J.JSApp (J.JSVar "require") [J.JSStringLiteral "./foreign"]
355 Nothing | requiresForeign m -> throwError . errorMessage $ MissingFFIModule mn
356 | otherwise -> return Nothing
357 pjs <- prettyPrintJS <$> J.moduleToJs m foreignInclude
358 let filePath = runModuleName mn
359 jsFile = outputDir </> filePath </> "index.js"
360 externsFile = outputDir </> filePath </> "externs.json"
361 foreignFile = outputDir </> filePath </> "foreign.js"
362 prefix = ["Generated by psc version " ++ showVersion Paths.version | usePrefix]
363 js = unlines $ map ("// " ++) prefix ++ [pjs]
365 writeTextFile jsFile (fromString js)
366 for_ (mn `M.lookup` foreigns) (readTextFile >=> writeTextFile foreignFile)
367 writeTextFile externsFile exts
369 requiresForeign :: CF.Module a -> Bool
370 requiresForeign = not . null . CF.moduleForeign
372 getTimestamp :: FilePath -> Make (Maybe UTCTime)
373 getTimestamp path = makeIO (const (ErrorMessage [] $ CannotGetFileInfo path)) $ do
374 exists <- doesFileExist path
375 traverse (const $ getModificationTime path) $ guard exists
377 readTextFile :: FilePath -> Make B.ByteString
378 readTextFile path = makeIO (const (ErrorMessage [] $ CannotReadFile path)) $ B.readFile path
380 writeTextFile :: FilePath -> B.ByteString -> Make ()
381 writeTextFile path text = makeIO (const (ErrorMessage [] $ CannotWriteFile path)) $ do
383 B.writeFile path text
385 mkdirp :: FilePath -> IO ()
386 mkdirp = createDirectoryIfMissing True . takeDirectory
388 progress :: ProgressMessage -> Make ()
389 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