on master help sign in
static: Reload static asset derivations when they change
77cbb3aAli Abrar
ChangeLog.md+2−1
@@ -8,7 +8,8 @@ This project's release branch is `master`. This log is written from the perspect
8 * Migration: All uses of `static @"some/path"` become `$(static "some/path")`. Instead of requiring `TypeApplications` and `DataKinds`, modules calling `static` must now enable `TemplateHaskell`.
8 * Migration: All uses of `static @"some/path"` become `$(static "some/path")`. Instead of requiring `TypeApplications` and `DataKinds`, modules calling `static` must now enable `TemplateHaskell`.
9 * Deprecation: Deprecate static asset modules generated via 'obelisk-asset-manifest-generate' in favor of modules generated via 'obelisk-asset-th-generate'. The new executable takes the same arguments as the old and should be a drop-in replacement. To preserve the old behavior, set `__deprecated.useObeliskAssetManifestGenerate = true;` in your obelisk project configuration.
9 * Deprecation: Deprecate static asset modules generated via 'obelisk-asset-manifest-generate' in favor of modules generated via 'obelisk-asset-th-generate'. The new executable takes the same arguments as the old and should be a drop-in replacement. To preserve the old behavior, set `__deprecated.useObeliskAssetManifestGenerate = true;` in your obelisk project configuration.
10 * Feature: Files added to the static directory while `ob run` is active no longer require `ob run` to be restarted
10 * Feature: Files added to the static directory while `ob run` is active no longer require `ob run` to be restarted
11−* When `staticFiles` is a derivation, as opposed to a regular directory, produce a symlink to the result of that derivation at `static.out` and have `ob run` serve static assets from that symlink. This makes is possible for the static asset derivation to be rebuilt and the new results served without restarting `ob run`.
11+* Feature: When `staticFiles` is a derivation, as opposed to a regular directory, produce a symlink to the result of that derivation at `static.out` and have `ob run` serve static assets from that symlink. This makes is possible for the static asset derivation to be rebuilt and the new results served without restarting `ob run`.
12+* Feature: Rebuild static asset derivations while `ob run` is active as long as the change to the derivation is within the project folder. `ob run` now displays a message ("Static assets rebuilt and symlinked to static.out") whenever static assets have been rebuilt and the new static assets are being served.
12
13
13## v0.9.1.0
14## v0.9.1.0
14
15
lib/command/obelisk-command.cabal+3
@@ -20,6 +20,7 @@ library
20 , exceptions
20 , exceptions
21 , extra
21 , extra
22 , filepath
22 , filepath
23+ , fsnotify
23 , git
24 , git
24 , github
25 , github
25 , here
26 , here
@@ -39,6 +40,8 @@ library
39 , optparse-applicative
40 , optparse-applicative
40 , placeholders
41 , placeholders
41 , process
42 , process
43+ , reflex
44+ , reflex-fsnotify
42 , shell-escape
45 , shell-escape
43 , temporary
46 , temporary
44 , terminal-size
47 , terminal-size
lib/command/src/Obelisk/Command/Project.hs+73−8
@@ -1,6 +1,9 @@
1{-# LANGUAGE FlexibleContexts #-}
1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE LambdaCase #-}
2{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE TupleSections #-}
6+
4module Obelisk.Command.Project
7module Obelisk.Command.Project
5 ( InitSource (..)
8 ( InitSource (..)
6 , findProjectObeliskCommand
9 , findProjectObeliskCommand
@@ -17,12 +20,16 @@ module Obelisk.Command.Project
17 , withProjectRoot
20 , withProjectRoot
18 , bashEscape
21 , bashEscape
19 , getHaskellManifestProjectPath
22 , getHaskellManifestProjectPath
23+ , AssetSource(..)
24+ , describeImpureAssetSource
25+ , watchStaticFilesDerivation
20 ) where
26 ) where
21
27
22import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
28import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
23−import Control.Lens ((.~), (?~), (<&>))
29+import Control.Lens ((.~), (?~), (<&>), (^.), _3)
24import Control.Monad
30import Control.Monad
25import Control.Monad.IO.Class (liftIO)
31import Control.Monad.IO.Class (liftIO)
32+import Control.Monad.Log
26import Control.Monad.State
33import Control.Monad.State
27import qualified Data.Aeson as Json
34import qualified Data.Aeson as Json
28import qualified Data.ByteString.UTF8 as BSU
35import qualified Data.ByteString.UTF8 as BSU
@@ -35,14 +42,19 @@ import Data.Text (Text)
35import qualified Data.Text as T
42import qualified Data.Text as T
36import Data.Text.Encoding (decodeUtf8, encodeUtf8)
43import Data.Text.Encoding (decodeUtf8, encodeUtf8)
37import Data.Traversable (for)
44import Data.Traversable (for)
45+import Reflex
46+import Reflex.FSNotify
47+import Reflex.Host.Headless
38import System.Directory
48import System.Directory
39import System.Environment (lookupEnv)
49import System.Environment (lookupEnv)
40import System.FilePath
50import System.FilePath
51+import System.FSNotify (defaultConfig)
41import System.IO.Temp
52import System.IO.Temp
42import System.IO.Unsafe (unsafePerformIO)
53import System.IO.Unsafe (unsafePerformIO)
43import System.PosixCompat.Files
54import System.PosixCompat.Files
44import System.PosixCompat.Types
55import System.PosixCompat.Types
45import System.PosixCompat.User
56import System.PosixCompat.User
57+import qualified System.Process as Proc
46import Text.ShellEscape (bash, bytes)
58import Text.ShellEscape (bash, bytes)
47
59
48import GitHub.Data.GitData (Branch)
60import GitHub.Data.GitData (Branch)
@@ -335,7 +347,21 @@ nixShellWithHoogle root isPure shell' command = do
335 \})).project.shells.${shell}"
347 \})).project.shells.${shell}"
336 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
348 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
337
349
350+-- | Describes the provenance of static assets (i.e., are they the result of a derivation
351+-- that was built, or just a folder full of files.
352+data AssetSource = AssetSource_Derivation
353+ | AssetSource_Files
354+ deriving (Eq)
355+
356+-- | Some log messages to make it easier to tell where static files are coming from
357+describeImpureAssetSource :: AssetSource -> Text -> Text
358+describeImpureAssetSource src path = case src of
359+ AssetSource_Files -> "Assets impurely loaded from: " <> path
360+ AssetSource_Derivation -> "Assets derivation built and impurely loaded from: " <> path
361+
362+-- | Determine where the static files of a project are and whether they're plain files or a derivation.
363+-- If they are a derivation, that derivation will be built.
338−findProjectAssets :: MonadObelisk m => FilePath -> m Text
364+findProjectAssets :: MonadObelisk m => FilePath -> m (AssetSource, Text)
339findProjectAssets root = do
365findProjectAssets root = do
340 isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
366 isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
341 proc nixExePath
367 proc nixExePath
@@ -349,14 +375,13 @@ findProjectAssets root = do
349 if isDerivation == "1"
375 if isDerivation == "1"
350 then do
376 then do
351 _ <- readProcessAndLogStderr Debug $ setCwd (Just root) $
377 _ <- readProcessAndLogStderr Debug $ setCwd (Just root) $
352− proc nixBuildExePath
378+ proc nixBuildExePath staticFilesDrvSymlinkArgs
353− [ "-o", "static.out"
379+
354− , "-E", "(import ./. {}).passthru.staticFilesImpure"
355− ]
356− pure $ T.pack $ root </> "static.out"
380+ pure (AssetSource_Derivation, T.pack $ root </> "static.out")
357− else readProcessAndLogStderr Debug $ setCwd (Just root) $
381+ else fmap (AssetSource_Files,) $ readProcessAndLogStderr Debug $ setCwd (Just root) $
358 proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
382 proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
359
383
384+-- | Get the nix store path to the generated static asset manifest module (e.g., "obelisk-generated-static")
360getHaskellManifestProjectPath :: MonadObelisk m => FilePath -> m Text
385getHaskellManifestProjectPath :: MonadObelisk m => FilePath -> m Text
361getHaskellManifestProjectPath root = fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
386getHaskellManifestProjectPath root = fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
362 proc nixBuildExePath
387 proc nixBuildExePath
@@ -364,3 +389,43 @@ getHaskellManifestProjectPath root = fmap T.strip $ readProcessAndLogStderr Debu
364 , "-E"
389 , "-E"
365 , "(let a = import ./. {}; in a.passthru.processedStatic.haskellManifest)"
390 , "(let a = import ./. {}; in a.passthru.processedStatic.haskellManifest)"
366 ]
391 ]
392+
393+staticFilesDrvSymlinkArgs :: [String]
394+staticFilesDrvSymlinkArgs =
395+ [ "-o", "static.out"
396+ , "-E", "(import ./. {}).passthru.staticFilesImpure"
397+ ]
398+
399+-- | Watch the project directory for file changes and check whether those file changes
400+-- cause changes in the static files nix derivation. If so, rebuild it.
401+watchStaticFilesDerivation
402+ :: MonadObelisk m
403+ => FilePath
404+ -> m ()
405+watchStaticFilesDerivation root = do
406+ cfg <- getCliConfig
407+ drv0 <- liftIO runShowDrv
408+ liftIO $ runHeadlessApp $ do
409+ pb <- getPostBuild
410+ checkForChanges <- watchDirectoryTree defaultConfig (root <$ pb) (const True)
411+ drv <- performEvent $ liftIO runShowDrv <$ checkForChanges
412+ drvs <- foldDyn (\new (_, old, _) -> (old, new, old /= new)) (drv0, drv0, False) drv
413+ out <- throttleBatchWithLag
414+ (\e -> performEvent $ ffor e $ \_ -> liftIO $ void runDrvBuild)
415+ (void $ ffilter (^._3) $ updated drvs)
416+ performEvent_ $ ffor out $ \_ ->
417+ runCli cfg $
418+ putLog Informational "Static assets rebuilt and symlinked to static.out"
419+ pure never
420+ where
421+ showDrv :: Proc.CreateProcess
422+ showDrv = (Proc.proc nixExePath
423+ [ "show-derivation"
424+ , "-f", "."
425+ , "passthru.staticFilesImpure"
426+ ]) { Proc.cwd = Just root }
427+ runShowDrv :: IO String
428+ runShowDrv = Proc.readCreateProcess showDrv ""
429+ runDrvBuild :: IO String
430+ runDrvBuild = Proc.readCreateProcess
431+ ((Proc.proc nixBuildExePath staticFilesDrvSymlinkArgs) { Proc.cwd = Just root }) ""
lib/command/src/Obelisk/Command/Run.hs+17−7
@@ -14,7 +14,8 @@ module Obelisk.Command.Run where
14import Control.Arrow ((&&&))
14import Control.Arrow ((&&&))
15import Control.Exception (Exception, bracket)
15import Control.Exception (Exception, bracket)
16import Control.Lens (ifor, (.~), (&))
16import Control.Lens (ifor, (.~), (&))
17−import Control.Monad (filterM)
17+import Control.Concurrent (forkIO)
18+import Control.Monad (filterM, void)
18import Control.Monad.Except (runExceptT, throwError)
19import Control.Monad.Except (runExceptT, throwError)
19import Control.Monad.IO.Class (liftIO)
20import Control.Monad.IO.Class (liftIO)
20import Control.Monad.Reader (MonadIO)
21import Control.Monad.Reader (MonadIO)
@@ -63,21 +64,23 @@ import System.FilePath
63import qualified System.Info
64import qualified System.Info
64import System.IO.Temp (withSystemTempDirectory)
65import System.IO.Temp (withSystemTempDirectory)
65
66
66−import Obelisk.App (MonadObelisk)
67+import Obelisk.App (MonadObelisk, getObelisk, runObelisk)
67import Obelisk.CliApp (
68import Obelisk.CliApp (
68 Severity (..),
69 Severity (..),
69 failWith,
70 failWith,
71+ getCliConfig,
70 proc,
72 proc,
71 putLog,
73 putLog,
72 readCreateProcessWithExitCode,
74 readCreateProcessWithExitCode,
73 readProcessAndLogStderr,
75 readProcessAndLogStderr,
76+ runCli,
74 runProcess_,
77 runProcess_,
75 setCwd,
78 setCwd,
76 setDelegateCtlc,
79 setDelegateCtlc,
77 withSpinner,
80 withSpinner,
78 )
81 )
79import Obelisk.Command.Nix
82import Obelisk.Command.Nix
80−import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets, bashEscape, getHaskellManifestProjectPath)
83+import Obelisk.Command.Project
81import Obelisk.Command.Thunk (attrCacheFileName)
84import Obelisk.Command.Thunk (attrCacheFileName)
82import Obelisk.Command.Utils (findExePath, ghcidExePath)
85import Obelisk.Command.Utils (findExePath, ghcidExePath)
83
86
@@ -144,8 +147,8 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
144 , _target_attr = Just "__unstable__.profiledObRun"
147 , _target_attr = Just "__unstable__.profiledObRun"
145 , _target_expr = Nothing
148 , _target_expr = Nothing
146 }
149 }
147− assets <- findProjectAssets root
150+ (assetType, assets) <- findProjectAssets root
148− putLog Debug $ "Assets impurely loaded from: " <> assets
151+ putLog Debug $ describeImpureAssetSource assetType assets
149 time <- liftIO getCurrentTime
152 time <- liftIO getCurrentTime
150 let profileBaseName = formatTime defaultTimeLocale profileBasePattern time
153 let profileBaseName = formatTime defaultTimeLocale profileBasePattern time
151 liftIO $ createDirectoryIfMissing True $ takeDirectory $ root </> profileBaseName
154 liftIO $ createDirectoryIfMissing True $ takeDirectory $ root </> profileBaseName
@@ -163,9 +166,16 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
163run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
166run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
164run root interpretPaths = do
167run root interpretPaths = do
165 pkgs <- getParsedLocalPkgs root interpretPaths
168 pkgs <- getParsedLocalPkgs root interpretPaths
166− assets <- findProjectAssets root
169+ (assetType, assets) <- findProjectAssets root
167 manifestPkg <- parsePackagesOrFail . (:[]) . T.unpack =<< getHaskellManifestProjectPath root
170 manifestPkg <- parsePackagesOrFail . (:[]) . T.unpack =<< getHaskellManifestProjectPath root
168− putLog Debug $ "Assets impurely loaded from: " <> assets
171+ putLog Debug $ describeImpureAssetSource assetType assets
172+ case assetType of
173+ AssetSource_Derivation -> do
174+ cliConfig <- getCliConfig
175+ ob <- getObelisk
176+ putLog Debug "Starting static file derivation watcher..."
177+ void $ liftIO $ forkIO $ runCli cliConfig $ runObelisk ob $ watchStaticFilesDerivation root
178+ _ -> pure ()
169 ghciArgs <- getGhciSessionSettings (pkgs <> manifestPkg) root True
179 ghciArgs <- getGhciSessionSettings (pkgs <> manifestPkg) root True
170 freePort <- getFreePort
180 freePort <- getFreePort
171 withGhciScriptArgs pkgs $ \dotGhciArgs -> do
181 withGhciScriptArgs pkgs $ \dotGhciArgs -> do