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.Trans.Except
47import Control.Monad.Reader
48import Control.Monad.Writer.Strict
49import Control.Monad.Supply
50import Control.Monad.Base (MonadBase(..))
51import Control.Monad.Trans.Control (MonadBaseControl(..))
53import Control.Concurrent.Lifted as C
55import Data.List (foldl', sort)
56import Data.Maybe (fromMaybe, catMaybes)
58import Data.String (fromString)
59import Data.Foldable (for_)
60#if __GLASGOW_HASKELL__ < 710
61import Data.Traversable (traverse)
63import Data.Traversable (for)
64import Data.Version (showVersion)
65import Data.Aeson (encode, decode)
66import qualified Data.ByteString.Lazy as B
67import qualified Data.Set as S
68import qualified Data.Map as M
70import System.Directory
71 (doesFileExist, getModificationTime, createDirectoryIfMissing)
72import System.FilePath ((</>), takeDirectory)
73import System.IO.Error (tryIOError)
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 (error "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 (warnings, externs) <- unzip . fromMaybe (error "make: externs were missing but no errors reported.") . sequence <$> for barriers (takeMVar . fst . snd)
180 tell (mconcat warnings)
181 return $ foldl' (flip applyExternsFileToEnvironment) initEnvironment externs
184 checkModuleNamesAreUnique :: m ()
185 checkModuleNamesAreUnique =
186 case findDuplicate (map getModuleName ms) of
188 Just mn -> throwError . errorMessage $ DuplicateModuleName mn
190 -- Verify that a list of values has unique keys
191 findDuplicate :: (Ord a) => [a] -> Maybe a
192 findDuplicate = go . sort
196 | otherwise = go (y : xs)
199 -- Sort a list so its elements appear in the same order as in another list.
200 inOrderOf :: (Ord a) => [a] -> [a] -> [a]
201 inOrderOf xs ys = let s = S.fromList xs in filter (`S.member` s) ys
203 buildModule :: [(ModuleName, (C.MVar (Maybe (MultipleErrors, ExternsFile)), C.MVar (Maybe MultipleErrors)))] -> Module -> [ModuleName] -> m ()
204 buildModule barriers m@(Module _ _ moduleName _ _) deps = flip catchError (markComplete Nothing . Just) $ do
205 -- We need to wait for dependencies to be built, before checking if the current
206 -- module should be rebuilt, so the first thing to do is to wait on the
207 -- MVars for the module's dependencies.
208 mexterns <- fmap unzip . sequence <$> mapM (readMVar . fst . fromMaybe (error "make: no barrier") . flip lookup barriers) deps
210 outputTimestamp <- getOutputTimestamp moduleName
211 dependencyTimestamp <- maximumMaybe <$> mapM (fmap shouldExist . getOutputTimestamp) deps
212 inputTimestamp <- getInputTimestamp moduleName
214 let shouldRebuild = case (inputTimestamp, dependencyTimestamp, outputTimestamp) of
215 (Right (Just t1), Just t3, Just t2) -> t1 > t2 || t3 > t2
216 (Right (Just t1), Nothing, Just t2) -> t1 > t2
217 (Left RebuildNever, _, Just _) -> False
222 Just (_, externs) -> do
223 (exts, warnings) <- listen $ do
224 progress $ CompilingModule moduleName
225 let env = foldl' (flip applyExternsFileToEnvironment) initEnvironment externs
227 ([desugared], nextVar) <- runSupplyT 0 $ desugar externs [m]
228 (checked@(Module ss coms _ elaborated exps), env') <- runCheck' env $ typeCheckModule desugared
229 checkExhaustiveModule env' checked
230 regrouped <- createBindingGroups moduleName . collapseBindingGroups $ elaborated
231 let mod' = Module ss coms moduleName regrouped exps
232 corefn = CF.moduleToCoreFn env' mod'
233 [renamed] = renameInModules [corefn]
234 exts = moduleToExternsFile mod' env'
235 evalSupplyT nextVar $ codegen renamed env' $ encode exts
237 markComplete (Just (warnings, exts)) Nothing
238 Nothing -> markComplete Nothing Nothing
243 mexts <- decodeExterns . snd <$> readExterns moduleName
245 Just exts -> markComplete (Just (mempty, exts)) Nothing
248 markComplete :: Maybe (MultipleErrors, ExternsFile) -> Maybe MultipleErrors -> m ()
249 markComplete externs errors = do
250 putMVar (fst $ fromMaybe (error "make: no barrier") $ lookup moduleName barriers) externs
251 putMVar (snd $ fromMaybe (error "make: no barrier") $ lookup moduleName barriers) errors
253 maximumMaybe :: (Ord a) => [a] -> Maybe a
254 maximumMaybe [] = Nothing
255 maximumMaybe xs = Just $ maximum xs
257 -- Make sure a dependency exists
258 shouldExist :: Maybe UTCTime -> UTCTime
259 shouldExist (Just t) = t
260 shouldExist _ = error "make: dependency should already have been built."
262 decodeExterns :: B.ByteString -> Maybe ExternsFile
263 decodeExterns bs = do
265 guard $ efVersion externs == showVersion Paths.version
269-- Add an import declaration for a module if it does not already explicitly import it.
271addDefaultImport :: ModuleName -> Module -> Module
272addDefaultImport toImport m@(Module ss coms mn decls exps) =
273 if isExistingImport `any` decls || mn == toImport then m
274 else Module ss coms mn (ImportDeclaration toImport Implicit Nothing : decls) exps
276 isExistingImport (ImportDeclaration mn' _ _) | mn' == toImport = True
277 isExistingImport (PositionedDeclaration _ _ d) = isExistingImport d
278 isExistingImport _ = False
280importPrim :: Module -> Module
281importPrim = addDefaultImport (ModuleName [ProperName C.prim])
284-- A monad for running make actions
286newtype Make a = Make { unMake :: ReaderT Options (WriterT MultipleErrors (ExceptT MultipleErrors IO)) a }
287 deriving (Functor, Applicative, Monad, MonadIO, MonadError MultipleErrors, MonadWriter MultipleErrors, MonadReader Options)
289instance MonadBase IO Make where
292instance MonadBaseControl IO Make where
293 type StM Make a = Either MultipleErrors (a, MultipleErrors)
294 liftBaseWith f = Make $ liftBaseWith $ \q -> f (q . unMake)
295 restoreM = Make . restoreM
298-- Execute a 'Make' monad, returning either errors, or the result of the compile plus any warnings.
300runMake :: Options -> Make a -> IO (Either MultipleErrors (a, MultipleErrors))
301runMake opts = runExceptT . runWriterT . flip runReaderT opts . unMake
303makeIO :: (IOError -> ErrorMessage) -> IO a -> Make a
305 e <- liftIO $ tryIOError io
306 either (throwError . singleError . f) return e
308-- Traverse (Either e) instance (base 4.7)
309traverseEither :: Applicative f => (a -> f b) -> Either e a -> f (Either e b)
310traverseEither _ (Left x) = pure (Left x)
311traverseEither f (Right y) = Right <$> f y
314-- A set of make actions that read and write modules from the given directory.
316buildMakeActions :: FilePath -- ^ the output directory
317 -> M.Map ModuleName (Either RebuildPolicy FilePath) -- ^ a map between module names and paths to the file containing the PureScript module
318 -> M.Map ModuleName FilePath -- ^ a map between module name and the file containing the foreign javascript for the module
319 -> Bool -- ^ Generate a prefix comment?
321buildMakeActions outputDir filePathMap foreigns usePrefix =
322 MakeActions getInputTimestamp getOutputTimestamp readExterns codegen progress
325 getInputTimestamp :: ModuleName -> Make (Either RebuildPolicy (Maybe UTCTime))
326 getInputTimestamp mn = do
327 let path = fromMaybe (error "Module has no filename in 'make'") $ M.lookup mn filePathMap
328 e1 <- traverseEither getTimestamp path
329 fPath <- maybe (return Nothing) getTimestamp $ M.lookup mn foreigns
330 return $ fmap (max fPath) e1
332 getOutputTimestamp :: ModuleName -> Make (Maybe UTCTime)
333 getOutputTimestamp mn = do
334 let filePath = runModuleName mn
335 jsFile = outputDir </> filePath </> "index.js"
336 externsFile = outputDir </> filePath </> "externs.json"
337 min <$> getTimestamp jsFile <*> getTimestamp externsFile
339 readExterns :: ModuleName -> Make (FilePath, B.ByteString)
341 let path = outputDir </> runModuleName mn </> "externs.json"
342 (path, ) <$> readTextFile path
344 codegen :: CF.Module CF.Ann -> Environment -> Externs -> SupplyT Make ()
345 codegen m _ exts = do
346 let mn = CF.moduleName m
347 foreignInclude <- case mn `M.lookup` foreigns of
349 | not $ requiresForeign m -> do
350 tell $ errorMessage $ UnnecessaryFFIModule mn path
352 | otherwise -> return $ Just $ J.JSApp (J.JSVar "require") [J.JSStringLiteral "./foreign"]
353 Nothing | requiresForeign m -> throwError . errorMessage $ MissingFFIModule mn
354 | otherwise -> return Nothing
355 pjs <- prettyPrintJS <$> J.moduleToJs m foreignInclude
356 let filePath = runModuleName mn
357 jsFile = outputDir </> filePath </> "index.js"
358 externsFile = outputDir </> filePath </> "externs.json"
359 foreignFile = outputDir </> filePath </> "foreign.js"
360 prefix = ["Generated by psc version " ++ showVersion Paths.version | usePrefix]
361 js = unlines $ map ("// " ++) prefix ++ [pjs]
363 writeTextFile jsFile (fromString js)
364 for_ (mn `M.lookup` foreigns) (readTextFile >=> writeTextFile foreignFile)
365 writeTextFile externsFile exts
367 requiresForeign :: CF.Module a -> Bool
368 requiresForeign = not . null . CF.moduleForeign
370 getTimestamp :: FilePath -> Make (Maybe UTCTime)
371 getTimestamp path = makeIO (const (ErrorMessage [] $ CannotGetFileInfo path)) $ do
372 exists <- doesFileExist path
373 traverse (const $ getModificationTime path) $ guard exists
375 readTextFile :: FilePath -> Make B.ByteString
376 readTextFile path = makeIO (const (ErrorMessage [] $ CannotReadFile path)) $ B.readFile path
378 writeTextFile :: FilePath -> B.ByteString -> Make ()
379 writeTextFile path text = makeIO (const (ErrorMessage [] $ CannotWriteFile path)) $ do
381 B.writeFile path text
383 mkdirp :: FilePath -> IO ()
384 mkdirp = createDirectoryIfMissing True . takeDirectory
386 progress :: ProgressMessage -> Make ()
387 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