Skip to content

Commit 16e906e

Browse files
committed
Add the --file-watch flag #113 (yay!)
1 parent 34fa687 commit 16e906e

File tree

8 files changed

+147
-28
lines changed

8 files changed

+147
-28
lines changed

ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* Print latest version of packages on conflicts [#450](https://github.com/commercialhaskell/stack/issues/450)
99
* Flag to avoid rerunning tests that haven't changed [#451](https://github.com/commercialhaskell/stack/issues/451)
1010
* stack can act as a script interpreter (see [Script interpreter] (https://github.com/commercialhaskell/stack/wiki/Script-interpreter) and [Reddit discussion](http://www.reddit.com/r/haskell/comments/3bd66h/stack_runghc_turtle_as_haskell_script_solution/))
11+
* Add the __`--file-watch`__ flag to auto-rebuild on file changes [#113](https://github.com/commercialhaskell/stack/issues/113)
1112

1213
## 0.1.1.0
1314

src/Stack/Build.hs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import Control.Monad.Trans.Resource
2626
import Data.Function
2727
import Data.Map.Strict (Map)
2828
import qualified Data.Map as Map
29+
import Data.Set (Set)
30+
import qualified Data.Set as Set
2931
import Network.HTTP.Client.Conduit (HasHttpManager)
32+
import Path
3033
import Path.IO
3134
import Prelude hiding (FilePath, writeFile)
3235
import Stack.Build.ConstructPlan
@@ -45,11 +48,22 @@ import Stack.Types.Internal
4548
type M env m = (MonadIO m,MonadReader env m,HasHttpManager env,HasBuildConfig env,MonadLogger m,MonadBaseControl IO m,MonadCatch m,MonadMask m,HasLogLevel env,HasEnvConfig env,HasTerminal env)
4649

4750
-- | Build
48-
build :: M env m => BuildOpts -> m ()
49-
build bopts = do
51+
build :: M env m
52+
=> (Set (Path Abs File) -> IO ()) -- ^ callback after discovering all local files
53+
-> BuildOpts
54+
-> m ()
55+
build setLocalFiles bopts = do
5056
menv <- getMinimalEnvOverride
5157

5258
(mbp, locals, extraToBuild, sourceMap) <- loadSourceMap bopts
59+
60+
-- Set local files, necessary for file watching
61+
stackYaml <- asks $ bcStackYaml . getBuildConfig
62+
liftIO $ setLocalFiles
63+
$ Set.insert stackYaml
64+
$ Set.unions
65+
$ map lpFiles locals
66+
5367
(installedMap, locallyRegistered) <-
5468
getInstalled menv
5569
GetInstalledOpts

src/Stack/Build/Source.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,13 @@ loadLocals bopts latestVersion = do
201201
when (packageName pkg /= name) $ throwM
202202
$ MismatchedCabalName cabalfp (packageName pkg)
203203
mbuildCache <- tryGetBuildCache dir
204+
files <- getPackageFiles (packageFiles pkg) AllFiles cabalfp
204205
fileModTimes <- getPackageFileModTimes pkg cabalfp
205206
return LocalPackage
206207
{ lpPackage = pkg
207208
, lpPackageFinal = pkgFinal
208209
, lpWanted = wanted
210+
, lpFiles = files
209211
, lpDirtyFiles =
210212
maybe True
211213
((/= fileModTimes) . buildCacheTimes)

src/Stack/Build/Types.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ data BuildOpts =
295295
,boptsCoverage :: !Bool
296296
-- ^ Enable code coverage report generation for test
297297
-- suites.
298+
,boptsFileWatch :: !Bool
299+
-- ^ Watch files for changes and automatically rebuild
298300
}
299301
deriving (Show)
300302

@@ -315,6 +317,7 @@ defaultBuildOpts = BuildOpts
315317
, boptsTestArgs = []
316318
, boptsOnlySnapshot = False
317319
, boptsCoverage = False
320+
, boptsFileWatch = False
318321
}
319322

320323
-- | Run a Setup.hs action after building a package, before installing.
@@ -353,6 +356,7 @@ data LocalPackage = LocalPackage
353356
, lpDir :: !(Path Abs Dir) -- ^ Directory of the package.
354357
, lpCabalFile :: !(Path Abs File) -- ^ The .cabal file
355358
, lpDirtyFiles :: !Bool -- ^ are there files that have changed since the last build?
359+
, lpFiles :: !(Set (Path Abs File)) -- ^ all files used by this package
356360
, lpComponents :: !(Set Text) -- ^ components to build, passed directly to Setup.hs build
357361
}
358362
deriving Show

src/Stack/FileWatch.hs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE TupleSections #-}
3+
module Stack.FileWatch
4+
( fileWatch
5+
, displayException
6+
) where
7+
8+
import Blaze.ByteString.Builder (toLazyByteString, copyByteString)
9+
import Blaze.ByteString.Builder.Char.Utf8 (fromShow)
10+
import Control.Concurrent.Async (race_)
11+
import Control.Concurrent.STM
12+
import Control.Exception (Exception)
13+
import Control.Exception.Enclosed (tryAny)
14+
import Control.Monad (forever, unless)
15+
import qualified Data.ByteString.Lazy as L
16+
import qualified Data.Map.Strict as Map
17+
import Data.Monoid ((<>))
18+
import Data.Set (Set)
19+
import qualified Data.Set as Set
20+
import Data.String (fromString)
21+
import Data.Traversable (forM)
22+
import Path
23+
import System.FSNotify
24+
import System.IO (stderr)
25+
26+
-- | Display an exception to stderr
27+
displayException :: Exception e => e -> IO ()
28+
displayException e =
29+
L.hPut stderr $ toLazyByteString $ fromShow e <> copyByteString "\n"
30+
31+
-- | Run an action, watching for file changes
32+
--
33+
-- The action provided takes a callback that is used to set the files to be
34+
-- watched. When any of those files are changed, we rerun the action again.
35+
fileWatch :: ((Set (Path Abs File) -> IO ()) -> IO ())
36+
-> IO ()
37+
fileWatch inner = withManager $ \manager -> do
38+
dirtyVar <- newTVarIO True
39+
watchVar <- newTVarIO Map.empty
40+
41+
let onChange = atomically $ writeTVar dirtyVar True
42+
43+
setWatched :: Set (Path Abs File) -> IO ()
44+
setWatched files = do
45+
watch0 <- readTVarIO watchVar
46+
let actions = Map.mergeWithKey
47+
keepListening
48+
stopListening
49+
startListening
50+
watch0
51+
newDirs
52+
watch1 <- forM (Map.toList actions) $ \(k, mmv) -> do
53+
mv <- mmv
54+
return $
55+
case mv of
56+
Nothing -> Map.empty
57+
Just v -> Map.singleton k v
58+
atomically $ writeTVar watchVar $ Map.unions watch1
59+
where
60+
newDirs = Map.fromList $ map (, ())
61+
$ Set.toList
62+
$ Set.map parent files
63+
64+
keepListening _dir listen () = Just $ return $ Just listen
65+
stopListening = Map.map $ \f -> do
66+
() <- f
67+
return Nothing
68+
startListening = Map.mapWithKey $ \dir () -> do
69+
let dir' = fromString $ toFilePath dir
70+
listen <- watchDir manager dir' (const True) (const onChange)
71+
return $ Just listen
72+
73+
let watchInput = do
74+
line <- getLine
75+
unless (line == "quit") $ do
76+
case line of
77+
"help" -> do
78+
putStrLn ""
79+
putStrLn "help: display this help"
80+
putStrLn "quit: exit"
81+
putStrLn "build: force a rebuild"
82+
putStrLn "watched: display watched directories"
83+
"build" -> onChange
84+
"watched" -> do
85+
watch <- readTVarIO watchVar
86+
mapM_ (putStrLn . toFilePath) (Map.keys watch)
87+
_ -> putStrLn $ "Unknown command: " ++ show line
88+
89+
watchInput
90+
91+
race_ watchInput $ forever $ do
92+
atomically $ do
93+
dirty <- readTVar dirtyVar
94+
check dirty
95+
writeTVar dirtyVar False
96+
97+
eres <- tryAny $ inner setWatched
98+
case eres of
99+
Left e -> displayException e
100+
Right () -> putStrLn "Success! Waiting for next file change."
101+
102+
putStrLn "Type help for available commands"

src/Stack/Upgrade.hs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,7 @@ upgrade fromGit mresolver = withSystemTempDirectory "stack-upgrade" $ \tmp' -> d
7272
(Just $ dir </> $(mkRelFile "stack.yaml"))
7373
lcLoadBuildConfig lc mresolver ThrowException
7474
envConfig1 <- runStackT manager logLevel bconfig terminal setupEnv
75-
runStackT manager logLevel envConfig1 terminal $ build BuildOpts
75+
runStackT manager logLevel envConfig1 terminal $ build (const $ return ()) defaultBuildOpts
7676
{ boptsTargets = ["stack"]
77-
, boptsLibProfile = False
78-
, boptsExeProfile = False
79-
, boptsEnableOptimizations = Nothing
80-
, boptsHaddock = False
81-
, boptsHaddockDeps = Nothing
82-
, boptsFinalAction = DoNothing
83-
, boptsDryrun = False
84-
, boptsGhcOptions = []
85-
, boptsFlags = Map.empty
8677
, boptsInstallExes = True
87-
, boptsPreFetch = False
88-
, boptsTestArgs = []
89-
, boptsOnlySnapshot = False
90-
, boptsCoverage = False
9178
}

src/main/Main.hs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,12 @@
99

1010
module Main where
1111

12-
import Blaze.ByteString.Builder (toLazyByteString, copyByteString)
13-
import Blaze.ByteString.Builder.Char.Utf8 (fromShow)
1412
import Control.Exception
1513
import Control.Monad
1614
import Control.Monad.IO.Class
1715
import Control.Monad.Logger
1816
import Control.Monad.Reader (ask)
1917
import Data.Attoparsec.Args (withInterpreterArgs)
20-
import qualified Data.ByteString.Lazy as L
2118
import Data.Char (toLower)
2219
import Data.List
2320
import qualified Data.List as List
@@ -47,6 +44,7 @@ import qualified Stack.Docker as Docker
4744
import Stack.Dot
4845
import Stack.Exec
4946
import Stack.Fetch
47+
import Stack.FileWatch
5048
import Stack.Init
5149
import Stack.New
5250
import qualified Stack.PackageIndex
@@ -256,7 +254,7 @@ main = withInterpreterArgs stackProgName $ \args isInterpreter ->
256254
case fromException e of
257255
Just ec -> exitWith ec
258256
Nothing -> do
259-
L.hPut stderr $ toLazyByteString $ fromShow e <> copyByteString "\n"
257+
displayException e
260258
exitFailure
261259
where
262260
dockerHelpOptName = Docker.dockerCmdName ++ "-help"
@@ -512,13 +510,17 @@ readFlag = do
512510

513511
-- | Build the project.
514512
buildCmd :: FinalAction -> BuildOpts -> GlobalOpts -> IO ()
515-
buildCmd finalAction opts go@GlobalOpts{..} = withBuildConfig go ThrowException $
516-
Stack.Build.build opts { boptsFinalAction = finalAction }
513+
buildCmd finalAction opts go
514+
| boptsFileWatch opts = fileWatch inner
515+
| otherwise = inner $ const $ return ()
516+
where
517+
inner setLocalFiles =
518+
withBuildConfig go ThrowException $
519+
Stack.Build.build setLocalFiles opts { boptsFinalAction = finalAction }
517520

518521
-- | Install
519522
installCmd :: BuildOpts -> GlobalOpts -> IO ()
520-
installCmd opts go@GlobalOpts{..} = withBuildConfig go ExecStrategy $
521-
Stack.Build.build opts { boptsInstallExes = True }
523+
installCmd opts = buildCmd DoNothing opts { boptsInstallExes = True }
522524

523525
-- | Unpack packages to the filesystem
524526
unpackCmd :: [String] -> GlobalOpts -> IO ()
@@ -591,7 +593,7 @@ execCmd :: ExecOpts -> GlobalOpts -> IO ()
591593
execCmd ExecOpts {..} go = withBuildConfig go ExecStrategy $ do
592594
let targets = concatMap words eoPackages
593595
unless (null targets) $ do
594-
Stack.Build.build defaultBuildOpts
596+
Stack.Build.build (const $ return ()) defaultBuildOpts
595597
{ boptsTargets = map T.pack targets
596598
}
597599
exec eoEnvSettings eoCmd eoArgs
@@ -649,7 +651,8 @@ buildOpts :: Command -> Parser BuildOpts
649651
buildOpts cmd = fmap process $
650652
BuildOpts <$> target <*> libProfiling <*> exeProfiling <*>
651653
optimize <*> haddock <*> haddockDeps <*> finalAction <*> dryRun <*> ghcOpts <*>
652-
flags <*> installExes <*> preFetch <*> testArgs <*> onlySnapshot <*> coverage
654+
flags <*> installExes <*> preFetch <*> testArgs <*> onlySnapshot <*> coverage <*>
655+
fileWatch'
653656
where process bopts =
654657
if boptsCoverage bopts
655658
then bopts { boptsExeProfile = True
@@ -729,6 +732,10 @@ buildOpts cmd = fmap process $
729732
help "Generate a code coverage report")
730733
else pure False
731734

735+
fileWatch' = flag False True
736+
(long "file-watch" <>
737+
help "Watch for changes in local files and automatically rebuild")
738+
732739
-- | Parser for docker cleanup arguments.
733740
dockerCleanupOpts :: Parser Docker.CleanupOpts
734741
dockerCleanupOpts =

stack.cabal

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ library
5050
Stack.Dot
5151
Stack.Fetch
5252
Stack.Exec
53+
Stack.FileWatch
5354
Stack.GhcPkg
5455
Stack.Init
5556
Stack.New
@@ -103,9 +104,10 @@ library
103104
, attoparsec >= 0.12.1.5
104105
, base >= 4 && <5
105106
, base16-bytestring
107+
, base64-bytestring
106108
, bifunctors >= 4.2.1
107109
, binary >= 0.7
108-
, base64-bytestring
110+
, blaze-builder
109111
, bytestring
110112
, conduit-combinators >= 0.3.1
111113
, conduit >= 1.2.4
@@ -118,6 +120,7 @@ library
118120
, exceptions >= 0.8.0.2
119121
, fast-logger >= 2.3.1
120122
, filepath >= 1.3.0.2
123+
, fsnotify
121124
, hashable >= 1.2.3.2
122125
, http-client >= 0.4.9
123126
, http-client-tls >= 0.2.2
@@ -168,7 +171,6 @@ executable stack
168171
Plugins.Commands
169172

170173
build-depends: base >=4.7 && < 5
171-
, blaze-builder
172174
, bytestring >= 0.10.4.0
173175
, containers
174176
, exceptions

0 commit comments

Comments
 (0)