1- {-# LANGUAGE NoImplicitPrelude #-}
2- {-# LANGUAGE OverloadedStrings #-}
1+ {-# LANGUAGE NoImplicitPrelude #-}
2+ {-# LANGUAGE OverloadedRecordDot #-}
3+ {-# LANGUAGE OverloadedStrings #-}
34
45module Stack.FileWatch
56 ( WatchMode (WatchModePoll )
@@ -11,25 +12,34 @@ import Control.Concurrent.STM ( check )
1112import qualified Data.Map.Merge.Strict as Map
1213import qualified Data.Map.Strict as Map
1314import qualified Data.Set as Set
15+ import qualified Data.Text as T
1416import GHC.IO.Exception
1517 ( IOErrorType (InvalidArgument ), IOException (.. ) )
16- import Path ( parent )
18+ import Path ( fileExtension , parent )
19+ import Path.IO ( doesFileExist , executable , getPermissions )
20+ import RIO.Process
21+ ( EnvVars , HasProcessContext (.. ), proc , runProcess
22+ , withModifyEnvVars
23+ )
24+ import System.Permissions ( osIsWindows )
1725import Stack.Prelude
26+ import Stack.Types.Config ( Config (.. ), HasConfig (.. ) )
27+ import Stack.Types.Runner ( HasRunner (.. ), Runner (.. ) )
1828import System.FSNotify
1929 ( WatchConfig , WatchMode (.. ), confWatchMode , defaultConfig
2030 , eventPath , watchDir , withManagerConf
2131 )
2232import System.IO ( getLine )
2333
2434fileWatch ::
25- HasTerm env
26- => ((Set (Path Abs File ) -> IO () ) -> RIO env () )
35+ ( HasConfig env , HasTerm env )
36+ => ((Set (Path Abs File ) -> IO () ) -> RIO Runner () )
2737 -> RIO env ()
2838fileWatch = fileWatchConf defaultConfig
2939
3040fileWatchPoll ::
31- HasTerm env
32- => ((Set (Path Abs File ) -> IO () ) -> RIO env () )
41+ ( HasConfig env , HasTerm env )
42+ => ((Set (Path Abs File ) -> IO () ) -> RIO Runner () )
3343 -> RIO env ()
3444fileWatchPoll =
3545 fileWatchConf $ defaultConfig { confWatchMode = WatchModePoll 1000000 }
@@ -39,11 +49,13 @@ fileWatchPoll =
3949-- The action provided takes a callback that is used to set the files to be
4050-- watched. When any of those files are changed, we rerun the action again.
4151fileWatchConf ::
42- HasTerm env
52+ ( HasConfig env , HasTerm env )
4353 => WatchConfig
44- -> ((Set (Path Abs File ) -> IO () ) -> RIO env () )
54+ -> ((Set (Path Abs File ) -> IO () ) -> RIO Runner () )
4555 -> RIO env ()
46- fileWatchConf cfg inner =
56+ fileWatchConf cfg inner = do
57+ runner <- view runnerL
58+ mHook <- view $ configL . to (. fileWatchHook)
4759 withRunInIO $ \ run -> withManagerConf cfg $ \ manager -> do
4860 allFiles <- newTVarIO Set. empty
4961 dirtyVar <- newTVarIO True
@@ -134,7 +146,7 @@ fileWatchConf cfg inner =
134146 dirty <- readTVar dirtyVar
135147 check dirty
136148
137- eres <- tryAny $ inner setWatched
149+ eres <- tryAny $ runRIO runner ( inner setWatched)
138150
139151 -- Clear dirtiness flag after the build to avoid an infinite loop caused
140152 -- by the build itself triggering dirtiness. This could be viewed as a
@@ -143,19 +155,63 @@ fileWatchConf cfg inner =
143155 -- https://github.com/commercialhaskell/stack/issues/822
144156 atomically $ writeTVar dirtyVar False
145157
146- case eres of
147- Left e ->
148- case fromException e of
149- Just ExitSuccess ->
150- prettyInfo $ style Good $ fromString $ displayException e
151- _ -> case fromException e :: Maybe PrettyException of
152- Just pe -> prettyError $ pretty pe
153- _ -> prettyInfo $ style Error $ fromString $ displayException e
154- _ -> prettyInfo $
155- style Good (flow " Success! Waiting for next file change." )
158+ let defaultAction = case eres of
159+ Left e ->
160+ case fromException e of
161+ Just ExitSuccess ->
162+ prettyInfo $ style Good $ fromString $ displayException e
163+ _ -> case fromException e :: Maybe PrettyException of
164+ Just pe -> prettyError $ pretty pe
165+ _ -> prettyInfo $ style Error $ fromString $ displayException e
166+ _ -> prettyInfo $
167+ style Good (flow " Success! Waiting for next file change." )
168+
169+ case mHook of
170+ Nothing -> defaultAction
171+ Just hook -> do
172+ hookIsExecutable <- handleIO (\ _ -> pure False ) $ if osIsWindows
173+ then
174+ -- can't really detect executable on windows, only file extension
175+ doesFileExist hook
176+ else executable <$> getPermissions hook
177+ if hookIsExecutable
178+ then runFileWatchHook eres hook
179+ else do
180+ prettyWarn $
181+ flow " File watch hook not executable. Falling back on default."
182+ defaultAction
156183
157184 prettyInfoL
158185 [ " Type"
159186 , style Shell " help"
160187 , flow " for the available commands. Press enter to force a rebuild."
161188 ]
189+
190+ runFileWatchHook ::
191+ (HasProcessContext env , HasTerm env )
192+ => Either SomeException ()
193+ -> Path Abs File
194+ -> RIO env ()
195+ runFileWatchHook buildResult hook =
196+ withModifyEnvVars insertBuildResultInEnv $ do
197+ let (cmd, args) = if osIsWindows && isShFile
198+ then (" sh" , [toFilePath hook])
199+ else (toFilePath hook, [] )
200+ menv <- view processContextL
201+ exit <- withProcessContext menv $ proc cmd args runProcess
202+ case exit of
203+ ExitSuccess -> pure ()
204+ ExitFailure i -> do
205+ prettyWarnL
206+ [ flow " File watch hook exited with code:"
207+ , style Error (fromString $ show i) <> " ."
208+ ]
209+ pure ()
210+ where
211+ insertBuildResultInEnv :: EnvVars -> EnvVars
212+ insertBuildResultInEnv = Map. insert " HOOK_FW_RESULT" $ case buildResult of
213+ Left e -> T. pack $ displayException e
214+ Right _ -> " "
215+ isShFile = case fileExtension hook of
216+ Just " .sh" -> True
217+ _ -> False
0 commit comments