@@ -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 #-}
4module Obelisk.Command.Project
7module Obelisk.Command.Project
5 ( InitSource (..)
8 ( InitSource (..)
6 , findProjectObeliskCommand
9 , findProjectObeliskCommand
@@ -16,12 +19,17 @@ module Obelisk.Command.Project
16 , toObeliskDir
19 , toObeliskDir
17 , withProjectRoot
20 , withProjectRoot
18 , bashEscape
21 , bashEscape
22+ , getHaskellManifestProjectPath
24+ , describeImpureAssetSource
25+ , watchStaticFilesDerivation
21import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
28import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
22−import Control.Lens ((.~), (?~), (<&>))
29+import Control.Lens ((.~), (?~), (<&>), (^.), _3)
23import Control.Monad
30import Control.Monad
24import Control.Monad.IO.Class (liftIO)
31import Control.Monad.IO.Class (liftIO)
32+import Control.Monad.Log
25import Control.Monad.State
33import Control.Monad.State
26import qualified Data.Aeson as Json
34import qualified Data.Aeson as Json
27import qualified Data.ByteString.UTF8 as BSU
35import qualified Data.ByteString.UTF8 as BSU
@@ -34,20 +42,25 @@ import Data.Text (Text)
34import qualified Data.Text as T
42import qualified Data.Text as T
35import Data.Text.Encoding (decodeUtf8, encodeUtf8)
43import Data.Text.Encoding (decodeUtf8, encodeUtf8)
36import Data.Traversable (for)
44import Data.Traversable (for)
46+import Reflex.FSNotify
47+import Reflex.Host.Headless
37import System.Directory
48import System.Directory
38import System.Environment (lookupEnv)
49import System.Environment (lookupEnv)
39import System.FilePath
50import System.FilePath
51+import System.FSNotify (defaultConfig)
40import System.IO.Temp
52import System.IO.Temp
41import System.IO.Unsafe (unsafePerformIO)
53import System.IO.Unsafe (unsafePerformIO)
42import System.PosixCompat.Files
54import System.PosixCompat.Files
43import System.PosixCompat.Types
55import System.PosixCompat.Types
44import System.PosixCompat.User
56import System.PosixCompat.User
57+import qualified System.Process as Proc
45import Text.ShellEscape (bash, bytes)
58import Text.ShellEscape (bash, bytes)
47import GitHub.Data.GitData (Branch)
60import GitHub.Data.GitData (Branch)
48import GitHub.Data.Name (Name)
61import GitHub.Data.Name (Name)
50−import Obelisk.App (MonadObelisk)
63+import Obelisk.App (MonadObelisk, runObelisk, getObelisk)
51import Obelisk.CliApp
64import Obelisk.CliApp
52import Obelisk.Command.Nix
65import Obelisk.Command.Nix
53import Obelisk.Command.Thunk
66import Obelisk.Command.Thunk
@@ -334,7 +347,21 @@ nixShellWithHoogle root isPure shell' command = do
334 \})).project.shells.${shell}"
347 \})).project.shells.${shell}"
335 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
348 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
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
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
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.
337−findProjectAssets :: MonadObelisk m => FilePath -> m Text
364+findProjectAssets :: MonadObelisk m => FilePath -> m (AssetSource, Text)
338findProjectAssets root = do
365findProjectAssets root = do
339 isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
366 isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
340 proc nixExePath
367 proc nixExePath
@@ -346,10 +373,62 @@ findProjectAssets root = do
347 -- Check whether the impure static files are a derivation (and so must be built)
374 -- Check whether the impure static files are a derivation (and so must be built)
348 if isDerivation == "1"
375 if isDerivation == "1"
377+ _ <- buildStaticFilesDerivationAndSymlink root
378+ pure (AssetSource_Derivation, T.pack $ root </> "static.out")
349− then fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ -- Strip whitespace here because nix-build has no --raw option
379+ else fmap (AssetSource_Files,) $ readProcessAndLogStderr Debug $ setCwd (Just root) $
380+ proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
382+-- | Get the nix store path to the generated static asset manifest module (e.g., "obelisk-generated-static")
383+getHaskellManifestProjectPath :: MonadObelisk m => FilePath -> m Text
384+getHaskellManifestProjectPath root = fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
350− proc nixBuildExePath
385+ proc nixBuildExePath
351− [ "--no-out-link"
386+ [ "--no-out-link"
388+ , "(let a = import ./. {}; in a.passthru.processedStatic.haskellManifest)"
391+-- | Watch the project directory for file changes and check whether those file changes
392+-- cause changes in the static files nix derivation. If so, rebuild it.
393+watchStaticFilesDerivation
394+ :: (MonadIO m, MonadObelisk m)
397+watchStaticFilesDerivation root = do
399+ drv0 <- showDerivation
400+ liftIO $ runHeadlessApp $ do
402+ checkForChanges <- watchDirectoryTree defaultConfig (root <$ pb) (const True)
403+ drv <- performEvent $ ffor checkForChanges $ \_ ->
404+ liftIO $ runObelisk ob showDerivation
405+ drvs <- foldDyn (\new (_, old, _) -> (old, new, old /= new)) (drv0, drv0, False) drv
406+ out <- throttleBatchWithLag
407+ (\e -> performEvent $ ffor e $ \_ -> liftIO $ runObelisk ob $
408+ buildStaticFilesDerivationAndSymlink root >> pure ())
409+ (void $ ffilter (^._3) $ updated drvs)
410+ performEvent_ $ ffor out $ \_ -> runObelisk ob $
411+ putLog Notice "Static assets rebuilt and symlinked to static.out"
414+ showDerivation :: MonadObelisk m => m Text
415+ showDerivation = readProcessAndLogStderr Debug $
416+ setCwd (Just root) $ ProcessSpec
417+ { _processSpec_createProcess = Proc.proc nixExePath
420+ , "passthru.staticFilesImpure"
422+ , _processSpec_overrideEnv = Nothing
425+buildStaticFilesDerivationAndSymlink :: MonadObelisk m => FilePath -> m Text
426+buildStaticFilesDerivationAndSymlink root = readProcessAndLogStderr Debug $
427+ setCwd (Just root) $ ProcessSpec
428+ { _processSpec_createProcess = Proc.proc
430+ [ "-o", "static.out"
352 , "-E", "(import ./. {}).passthru.staticFilesImpure"
431 , "-E", "(import ./. {}).passthru.staticFilesImpure"
354− else readProcessAndLogStderr Debug $ setCwd (Just root) $
433+ , _processSpec_overrideEnv = Nothing
355− proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
434+ }