{-# LANGUAGE RecordWildCards, NamedFieldPuns, ScopedTypeVariables, ConstraintKinds, TupleSections #-}
module Development.Shake.Internal.Core.Action(
actionOnException, actionFinally, actionBracket, actionCatch, actionRetry,
getShakeOptions, getProgress, runAfter,
lintTrackRead, lintTrackWrite, lintTrackAllow,
getVerbosity, putWhen, putVerbose, putInfo, putWarn, putError, withVerbosity, quietly,
orderOnlyAction,
newCacheIO,
unsafeExtraThread,
parallel,
batch,
reschedule,
historyDisable,
traced,
producesChecked, producesUnchecked, producesCheck, lintCurrentDirectory, lintWatch,
blockApply, unsafeAllowApply, shakeException, lintTrackFinished,
getCurrentKey, getLocal,
actionShareList, actionShareRemove, actionShareSanity
) where
import Control.Exception
import Control.Monad.Extra
import Control.Monad.IO.Class
import Control.DeepSeq
import Data.Typeable
import System.Directory
import System.FilePattern
import System.FilePattern.Directory
import Control.Concurrent.Extra
import Data.Maybe
import Data.Tuple.Extra
import Data.IORef
import Data.List.Extra
import Numeric.Extra
import General.Extra
import qualified Data.HashMap.Strict as Map
import qualified Data.HashSet as Set
import Development.Shake.Classes
import Development.Shake.Internal.Core.Monad
import Development.Shake.Internal.Core.Database
import Development.Shake.Internal.History.Shared
import General.Pool
import Development.Shake.Internal.Core.Types
import Development.Shake.Internal.Core.Rules
import Development.Shake.Internal.Core.Pool
import Development.Shake.Internal.Value
import Development.Shake.Internal.FileInfo
import Development.Shake.Internal.FileName
import Development.Shake.Internal.Options
import Development.Shake.Internal.Errors
import General.Cleanup
import General.Fence
actionThenUndoLocal :: (Local -> (Local, Local -> Local)) -> Action a -> Action a
actionThenUndoLocal f m = Action $ do
s <- getRW
let (s2,undo) = f s
putRW s2
res <- fromAction m
modifyRW undo
return res
shakeException :: Global -> Stack -> SomeException -> IO ShakeException
shakeException Global{globalOptions=ShakeOptions{..},..} stk e = case fromException e of
Just (e :: ShakeException) -> return e
Nothing -> do
e <- return $ exceptionStack stk e
when (shakeStaunch && shakeVerbosity >= Error) $
globalOutput Error $ show e ++ "Continuing due to staunch mode"
return e
actionBracketEx :: Bool -> IO a -> (a -> IO b) -> (a -> Action c) -> Action c
actionBracketEx runOnSuccess alloc free act = do
Global{..} <- Action getRO
(v, key) <- liftIO $ mask_ $ do
v <- alloc
key <- liftIO $ register globalCleanup $ void $ free v
return (v, key)
res <- Action $ catchRAW (fromAction $ act v) $ \e -> liftIO (release key) >> throwRAW e
liftIO $ if runOnSuccess then release key else unprotect key
return res
actionOnException :: Action a -> IO b -> Action a
actionOnException act free = actionBracketEx False (return ()) (const free) (const act)
actionFinally :: Action a -> IO b -> Action a
actionFinally act free = actionBracket (return ()) (const free) (const act)
actionBracket :: IO a -> (a -> IO b) -> (a -> Action c) -> Action c
actionBracket = actionBracketEx True
actionCatch :: Exception e => Action a -> (e -> Action a) -> Action a
actionCatch act hdl = Action $ catchRAW (fromAction act) $ \e ->
case () of
_ | not $ isAsyncException e
, Nothing <- fromException e :: Maybe ShakeException
, Just e <- fromException e
-> fromAction $ hdl e
_ -> throwRAW e
actionRetry :: Int -> Action a -> Action a
actionRetry i act
| i <= 0 = fail $ "actionRetry first argument must be positive, got " ++ show i
| i == 1 = act
| otherwise = Action $ catchRAW (fromAction act) $ \_ -> fromAction $ actionRetry (i-1) act
getShakeOptions :: Action ShakeOptions
getShakeOptions = Action $ globalOptions <$> getRO
getProgress :: Action Progress
getProgress = do
Global{..} <- Action getRO
liftIO globalProgress
runAfter :: IO () -> Action ()
runAfter op = do
Global{..} <- Action getRO
liftIO $ atomicModifyIORef globalAfter $ \ops -> (op:ops, ())
putWhen :: Verbosity -> String -> Action ()
putWhen v msg = do
Global{..} <- Action getRO
verb <- getVerbosity
when (verb >= v) $
liftIO $ globalOutput v msg
putVerbose :: String -> Action ()
putVerbose = putWhen Verbose
putInfo :: String -> Action ()
putInfo = putWhen Info
putWarn :: String -> Action ()
putWarn = putWhen Warn
putError :: String -> Action ()
putError = putWhen Error
getVerbosity :: Action Verbosity
getVerbosity = Action $ localVerbosity <$> getRW
withVerbosity :: Verbosity -> Action a -> Action a
withVerbosity new = actionThenUndoLocal $ \s0 ->
(s0{localVerbosity=new}, \s -> s{localVerbosity=localVerbosity s0})
quietly :: Action a -> Action a
quietly = withVerbosity Error
unsafeAllowApply :: Action a -> Action a
unsafeAllowApply = applyBlockedBy Nothing
blockApply :: String -> Action a -> Action a
blockApply = applyBlockedBy . Just
applyBlockedBy :: Maybe String -> Action a -> Action a
applyBlockedBy reason = actionThenUndoLocal $ \s0 ->
(s0{localBlockApply=reason}, \s -> s{localBlockApply=localBlockApply s0})
traced :: String -> IO a -> Action a
traced msg act = do
Global{..} <- Action getRO
Local{localStack} <- Action getRW
start <- liftIO globalTimestamp
let key = showTopStack localStack
putInfo $ "# " ++ msg ++ " (for " ++ key ++ ")"
res <- liftIO $
(shakeTrace globalOptions key msg True >> act)
`finally` shakeTrace globalOptions key msg False
stop <- liftIO globalTimestamp
let trace = newTrace msg start stop
liftIO $ evaluate $ rnf trace
Action $ modifyRW $ \s -> s{localTraces = trace : localTraces s}
return res
lintTrackRead :: ShakeValue key => [key] -> Action ()
lintTrackRead ks = do
Global{..} <- Action getRO
when (isJust $ shakeLint globalOptions) $ do
l@Local{..} <- Action getRW
deps <- liftIO $ concatMapM (listDepends globalDatabase) localDepends
let top = topStack localStack
let condition1 k = top == Just k
let condition2 k = k `elem` deps
let condition3 k = any ($ k) localTrackAllows
let condition4 = filter (\k -> not $ condition1 k || condition2 k || condition3 k) $ map newKey ks
unless (null condition4) $
Action $ putRW l{localTrackRead = condition4 ++ localTrackRead}
lintTrackWrite :: ShakeValue key => [key] -> Action ()
lintTrackWrite ks = do
Global{..} <- Action getRO
when (isJust $ shakeLint globalOptions) $ do
l@Local{..} <- Action getRW
let top = topStack localStack
let condition1 k = Just k == top
let condition2 k = any ($ k) localTrackAllows
let condition3 = filter (\k -> not $ condition1 k || condition2 k) $ map newKey ks
unless (null condition3) $
Action $ putRW l{localTrackWrite = condition3 ++ localTrackWrite}
lintTrackFinished :: Action ()
lintTrackFinished = do
Global{..} <- Action getRO
Local{..} <- Action getRW
liftIO $ do
let top = topStack localStack
let ignore k = any ($ k) localTrackAllows
deps <- concatMapM (listDepends globalDatabase) localDepends
let used = Set.filter (not . ignore) $ Set.fromList localTrackRead
bad <- return $ Set.toList $ used `Set.difference` Set.fromList deps
unless (null bad) $ do
let n = length bad
throwM $ errorStructured
("Lint checking error - " ++ (if n == 1 then "value was" else show n ++ " values were") ++ " used but not depended upon")
[("Used", Just $ show x) | x <- bad]
""
bad <- flip filterM (Set.toList used) $ \k -> not . null <$> lookupDependencies globalDatabase k
unless (null bad) $ do
let n = length bad
throwM $ errorStructured
("Lint checking error - " ++ (if n == 1 then "value was" else show n ++ " values were") ++ " depended upon after being used")
[("Used", Just $ show x) | x <- bad]
""
bad <- return $ filter (not . ignore) $ Set.toList $ Set.fromList localTrackWrite
unless (null bad) $
liftIO $ atomicModifyIORef globalTrackAbsent $ \old -> ([(fromMaybe k top, k) | k <- bad] ++ old, ())
lintTrackAllow :: ShakeValue key => (key -> Bool) -> Action ()
lintTrackAllow (test :: key -> Bool) = do
Global{..} <- Action getRO
when (isJust $ shakeLint globalOptions) $
Action $ modifyRW $ \s -> s{localTrackAllows = f : localTrackAllows s}
where
tk = typeRep (Proxy :: Proxy key)
f k = typeKey k == tk && test (fromKey k)
lintCurrentDirectory :: FilePath -> String -> IO ()
lintCurrentDirectory old msg = do
now <- getCurrentDirectory
when (old /= now) $ throwIO $ errorStructured
"Lint checking error - current directory has changed"
[("When", Just msg)
,("Wanted",Just old)
,("Got",Just now)]
""
lintWatch :: [FilePattern] -> IO (String -> IO ())
lintWatch [] = return $ const $ return ()
lintWatch pats = do
let op = getDirectoryFiles "." pats
let record = do xs <- op; forM xs $ \x -> (x,) <$> getFileInfo (fileNameFromString x)
old <- record
return $ \msg -> do
now <- record
when (old /= now) $ throwIO $ errorStructured
"Lint checking error - watched files have changed"
(("When", Just msg) : changes (Map.fromList old) (Map.fromList now))
""
where
changes old now =
[("Created", Just x) | x <- Map.keys $ Map.difference now old] ++
[("Deleted", Just x) | x <- Map.keys $ Map.difference old now] ++
[("Changed", Just x) | x <- Map.keys $ Map.filter id $ Map.intersectionWith (/=) old now]
listDepends :: Database -> Depends -> IO [Key]
listDepends db (Depends xs) = mapM (fmap (fst . fromJust) . getKeyValueFromId db) xs
lookupDependencies :: Database -> Key -> IO [Depends]
lookupDependencies db k = do
Just (Ready r) <- getValueFromKey db k
return $ depends r
historyDisable :: Action ()
historyDisable = Action $ modifyRW $ \s -> s{localHistory = False}
producesChecked :: [FilePath] -> Action ()
producesChecked xs = Action $ modifyRW $ \s -> s{localProduces = map (True,) (reverse xs) ++ localProduces s}
producesUnchecked :: [FilePath] -> Action ()
producesUnchecked xs = Action $ modifyRW $ \s -> s{localProduces = map (False,) (reverse xs) ++ localProduces s}
producesCheck :: Action ()
producesCheck = do
Local{localProduces} <- Action getRW
missing <- liftIO $ filterM (notM . doesFileExist_) $ map snd $ filter fst localProduces
when (missing /= []) $ throwM $ errorStructured
"Files declared by 'produces' not produced"
[("File " ++ show i, Just x) | (i,x) <- zipFrom 1 missing]
""
orderOnlyAction :: Action a -> Action a
orderOnlyAction act = Action $ do
Local{localDepends=pre} <- getRW
res <- fromAction act
modifyRW $ \s -> s{localDepends=pre}
return res
newCacheIO :: (Eq k, Hashable k) => (k -> Action v) -> IO (k -> Action v)
newCacheIO (act :: k -> Action v) = do
var :: Var (Map.HashMap k (Fence IO (Either SomeException ([Depends],v)))) <- newVar Map.empty
return $ \key ->
join $ liftIO $ modifyVar var $ \mp -> case Map.lookup key mp of
Just bar -> return $ (,) mp $ do
(offset, (deps, v)) <- actionFenceRequeue bar
Action $ modifyRW $ \s -> addDiscount offset $ s{localDepends = deps ++ localDepends s}
return v
Nothing -> do
bar <- newFence
return $ (Map.insert key bar mp,) $ do
Local{localDepends=pre} <- Action getRW
res <- Action $ tryRAW $ fromAction $ act key
case res of
Left err -> do
liftIO $ signalFence bar $ Left err
Action $ throwRAW err
Right v -> do
Local{localDepends=post} <- Action getRW
let deps = dropEnd (length pre) post
liftIO $ signalFence bar $ Right (deps, v)
return v
unsafeExtraThread :: Action a -> Action a
unsafeExtraThread act = do
Global{..} <- Action getRO
stop <- liftIO $ increasePool globalPool
res <- Action $ tryRAW $ fromAction $ blockApply "Within unsafeExtraThread" act
liftIO stop
(wait, res) <- actionAlwaysRequeue res
Action $ modifyRW $ addDiscount wait
return res
parallel :: [Action a] -> Action [a]
parallel [] = return []
parallel [x] = return <$> x
parallel acts = do
Global{..} <- Action getRO
done <- liftIO $ newIORef False
waits <- forM acts $ \act ->
addPoolWait PoolResume $ do
whenM (liftIO $ readIORef done) $
fail "parallel, one has already failed"
Action $ modifyRW localClearMutable
res <- act
old <- Action getRW
return (old, res)
(wait, res) <- actionFenceSteal =<< liftIO (exceptFence waits)
liftIO $ atomicWriteIORef done True
let (waits, locals, results) = unzip3 $ map (\(a,(b,c)) -> (a,b,c)) res
Action $ modifyRW $ \root -> addDiscount (wait - sum waits) $ localMergeMutable root locals
return results
batch
:: Int
-> ((a -> Action ()) -> Rules ())
-> (a -> Action b)
-> ([b] -> Action ())
-> Rules ()
batch mx pred one many
| mx <= 0 = error $ "Can't call batchable with <= 0, you used " ++ show mx
| mx == 1 = pred $ \a -> do b <- one a; many [b]
| otherwise = do
todo :: IORef (Int, [(b, Local, Fence IO (Either SomeException Local))]) <- liftIO $ newIORef (0, [])
pred $ \a -> do
b <- one a
fence <- liftIO newFence
local <- Action getRW
count <- liftIO $ atomicModifyIORef todo $ \(count, bs) -> let i = count+1 in ((i, (b,local,fence):bs), i)
requeue todo (==) count
(wait, local2) <- actionFenceRequeue fence
Action $ modifyRW $ \root -> addDiscount wait $ localMergeMutable root [local2]
where
requeue todo trigger count
| count `trigger` mx = addPoolWait_ PoolResume $ go todo
| count `trigger` 1 = addPoolWait_ PoolBatch $ go todo
| otherwise = return ()
go todo = do
(now, count) <- liftIO $ atomicModifyIORef todo $ \(count, bs) ->
let (now,later) = splitAt mx bs
count2 = if count > mx then count - mx else 0
in ((count2, later), (now, count2))
requeue todo (>=) count
unless (null now) $ do
res <- Action $ tryRAW $ do
modifyRW $ const $ localClearMutable $ snd3 $ head now
fromAction $ many $ map fst3 now
res <- getRW
return res{localDiscount = localDiscount res / intToDouble (length now)}
liftIO $ mapM_ (flip signalFence res . thd3) now
reschedule :: Double -> Action ()
reschedule x = do
(wait, _) <- actionAlwaysRequeuePriority (PoolDeprioritize $ negate x) $ return ()
Action $ modifyRW $ addDiscount wait
getCurrentKey :: Action (Maybe Key)
getCurrentKey = Action $ topStack . localStack <$> getRW
getLocal :: Action Local
getLocal = Action getRW
actionShareRemove :: [String] -> Action ()
actionShareRemove substrs = do
Global{..} <- Action getRO
case globalShared of
Nothing -> throwM $ errorInternal "actionShareRemove with no shared"
Just x -> liftIO $ removeShared x $ \k -> any (`isInfixOf` show k) substrs
actionShareList :: Action ()
actionShareList = do
Global{..} <- Action getRO
case globalShared of
Nothing -> throwM $ errorInternal "actionShareList with no shared"
Just x -> liftIO $ listShared x
actionShareSanity :: Action ()
actionShareSanity = do
Global{..} <- Action getRO
case globalShared of
Nothing -> throwM $ errorInternal "actionShareSanity with no shared"
Just x -> liftIO $ sanityShared x