on master help sign in
Merge pull request #489 from rvl/extra-local-packages

ob run: Query local packages from Nix project

commit796fac56e6a96d2beecca012290063e662ddbbaf authorElliot Cameron <3noch@users.noreply.github.com> dateFri 24 Jan 2020, 01:04 (7 years ago) parent41d710bfb3ffb3
20 files changed+565−281
ChangeLog.md+1 README.md+17 default.nix+105−76 lib/asset/serve-snap/src/Obelisk/Asset/Accept.hs+6−6 lib/backend/src/Obelisk/Backend.hs−2 lib/cliapp/obelisk-cliapp.cabal+2−1 lib/cliapp/src/Obelisk/CliApp.hs+1 lib/cliapp/src/Obelisk/CliApp/Logging.hs+11−10 lib/cliapp/src/Obelisk/CliApp/Process.hs+15 lib/command/obelisk-command.cabal+9−7 lib/command/src/Obelisk/Command.hs+14−25 lib/command/src/Obelisk/Command/Nix.hs+44−13 lib/command/src/Obelisk/Command/Preprocessor.hs+93 lib/command/src/Obelisk/Command/Project.hs+55−19 lib/command/src/Obelisk/Command/Run.hs+174−105 lib/command/src/Obelisk/Command/Utils.hs+12−5 lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+1−8 lib/frontend/src/Obelisk/Frontend.hs+2−2 lib/frontend/src/Obelisk/Frontend/Cookie.hs+2−1 lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1
ChangeLog.md+1
@@ -5,6 +5,7 @@ This project's release branch is `master`. This log is written from the perspect
55## Unreleased
66
77* Add `Obelisk.Route.(?/)`, a convenience function for constructing routes nested in `Maybe`. ([#457](https://github.com/obsidiansystems/obelisk/pull/457))
8+* Add local packages from the Nix `project { packages }` option to the GHCi config, so that they are auto-reloaded with `ob run`. ([#489](https://github.com/obsidiansystems/obelisk/pull/489))
89
910## v0.4.0.0 - 2020-01-10
1011
README.md+17
@@ -144,6 +144,23 @@ project ./. ({ pkgs, ... }: {
144144
145145For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
146146
147+### Adding extra local packages
148+
149+If the standard packages (`frontend`, `backend`, and `common`) are not
150+enough, to add more local Haskell packages, define them with the
151+`packages` parameter. The sources of these packages will be
152+automatically reloaded by `ob run`.
153+
154+```nix
155+# ...
156+project ./. ({ pkgs, ... }: {
157+# ...
158+ packages = {
159+ another = ./another;
160+ };
161+# ...
162+```
163+
147164### Running over https
148165
149166To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
default.nix+105−76
@@ -257,85 +257,114 @@ in rec {
257257 };
258258 };
259259
260−
261260 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
262− project = base: projectDefinition:
263− let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
264− let mkProject = { android ? null #TODO: Better error when missing
265− , ios ? null #TODO: Better error when missing
266− , packages ? {}
267− , overrides ? _: _: {}
268− , staticFiles ? base + /static
269− , tools ? _: []
270− , shellToolOverrides ? _: _: {}
271− , withHoogle ? false # Setting this to `true` makes shell reloading far slower
272− , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
273− }:
274− let frontendName = "frontend";
275− backendName = "backend";
276− commonName = "common";
277− staticName = "obelisk-generated-static";
278− staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
279− processedStatic = processAssets { src = staticFiles; };
280− # The packages whose names and roles are defined by this package
281− predefinedPackages = lib.filterAttrs (_: x: x != null) {
282− ${frontendName} = nullIfAbsent (base + "/frontend");
283− ${commonName} = nullIfAbsent (base + "/common");
284− ${backendName} = nullIfAbsent (base + "/backend");
285− };
286− combinedPackages = predefinedPackages // packages;
287− projectOverrides = self: super: {
288− ${staticName} = haskellLib.dontHaddock (self.callCabal2nix staticName processedStatic.haskellManifest {});
289− ${backendName} = haskellLib.addBuildDepend super.${backendName} self.obelisk-run;
290− };
291− totalOverrides = lib.composeExtensions projectOverrides overrides;
292− inherit (lib.strings) hasPrefix;
293− privateConfigDirs = ["config/backend"];
294− injectableConfig = builtins.filterSource (path: _:
295− !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
296− );
297− __androidWithConfig = configPath: {
298− ${if android == null then null else frontendName} = {
299− executableName = "frontend";
300− ${if builtins.pathExists staticFiles then "assets" else null} =
301− nixpkgs.obeliskExecutableConfig.platforms.android.inject
302− (injectableConfig configPath)
303− processedStatic.symlinked;
304− } // android;
261+ project = base': projectDefinition:
262+ let
263+ projectOut = sys: let reflexPlatformProject = (getReflexPlatform sys).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
264+ let
265+ inherit (lib.strings) hasPrefix;
266+ mkProject =
267+ { android ? null #TODO: Better error when missing
268+ , ios ? null #TODO: Better error when missing
269+ , packages ? {}
270+ , overrides ? _: _: {}
271+ , staticFiles ? null
272+ , tools ? _: []
273+ , shellToolOverrides ? _: _: {}
274+ , withHoogle ? false # Setting this to `true` makes shell reloading far slower
275+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
276+ }:
277+ let
278+ allConfig = nixpkgs.lib.makeExtensible (self: {
279+ base = base';
280+ inherit args;
281+ userSettings = {
282+ inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel;
283+ staticFiles = if staticFiles == null then self.base + /static else staticFiles;
284+ };
285+ frontendName = "frontend";
286+ backendName = "backend";
287+ commonName = "common";
288+ staticName = "obelisk-generated-static";
289+ staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
290+ processedStatic = processAssets { src = self.userSettings.staticFiles; };
291+ # The packages whose names and roles are defined by this package
292+ predefinedPackages = lib.filterAttrs (_: x: x != null) {
293+ ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
294+ ${self.commonName} = nullIfAbsent (self.base + "/common");
295+ ${self.backendName} = nullIfAbsent (self.base + "/backend");
296+ };
297+ shellPackages = {};
298+ combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
299+ projectOverrides = self': super': {
300+ ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
301+ ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
302+ };
303+ totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
304+ privateConfigDirs = ["config/backend"];
305+ injectableConfig = builtins.filterSource (path: _:
306+ !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
307+ );
308+ __androidWithConfig = configPath: {
309+ ${if self.userSettings.android == null then null else self.frontendName} = {
310+ executableName = "frontend";
311+ ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
312+ nixpkgs.obeliskExecutableConfig.platforms.android.inject
313+ (self.injectableConfig configPath)
314+ self.processedStatic.symlinked;
315+ } // self.userSettings.android;
316+ };
317+ __iosWithConfig = configPath: {
318+ ${if self.userSettings.ios == null then null else self.frontendName} = {
319+ executableName = "frontend";
320+ ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} =
321+ nixpkgs.obeliskExecutableConfig.platforms.ios.inject
322+ (self.injectableConfig configPath)
323+ self.processedStatic.symlinked;
324+ } // self.userSettings.ios;
325+ };
326+
327+ shells-ghc = builtins.attrNames self.shellPackages;
328+
329+ shells-ghcjs = [
330+ self.frontendName
331+ self.commonName
332+ ];
333+
334+ shells-ghcSavedSplices = [
335+ self.commonName
336+ self.frontendName
337+ ];
338+
339+ project = reflexPlatformProject ({...}: self.projectConfig);
340+ projectConfig = {
341+ inherit (self.userSettings) shellToolOverrides tools withHoogle;
342+ overrides = self.totalOverrides;
343+ packages = self.combinedPackages;
344+ shells = {
345+ ${if self.userSettings.android == null && self.userSettings.ios == null then null else "ghcSavedSplices"} =
346+ lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
347+ ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
348+ ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
305349 };
306− __iosWithConfig = configPath: {
307− ${if ios == null then null else frontendName} = {
308− executableName = "frontend";
309− ${if builtins.pathExists staticFiles then "staticSrc" else null} =
310− nixpkgs.obeliskExecutableConfig.platforms.ios.inject
311− (injectableConfig configPath)
312− processedStatic.symlinked;
313− } // ios;
350+ android = self.__androidWithConfig (self.base + "/config");
351+ ios = self.__iosWithConfig (self.base + "/config");
352+
353+ passthru = {
354+ __unstable__.self = allConfig;
355+ inherit (self)
356+ staticFilesImpure processedStatic
357+ __iosWithConfig __androidWithConfig
358+ ;
359+ inherit (self.userSettings)
360+ android ios overrides packages shellToolOverrides staticFiles tools withHoogle
361+ __closureCompilerOptimizationLevel
362+ ;
314363 };
315− in {
316− inherit shellToolOverrides tools withHoogle;
317− overrides = totalOverrides;
318− packages = combinedPackages;
319− shells = {
320− ${if android == null && ios == null then null else "ghcSavedSplices"} = (lib.filter (x: lib.hasAttr x combinedPackages) [
321− commonName
322− frontendName
323− ]);
324− ghc = (lib.filter (x: lib.hasAttr x combinedPackages) [
325− backendName
326− commonName
327− frontendName
328− ]);
329− ghcjs = lib.filter (x: lib.hasAttr x combinedPackages) [
330− frontendName
331− commonName
332− ];
333364 };
334− android = __androidWithConfig (base + "/config");
335− ios = __iosWithConfig (base + "/config");
336− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
337− };
338− in mkProject (projectDefinition args));
365+ });
366+ in allConfig;
367+ in (mkProject (projectDefinition args)).projectConfig);
339368 mainProjectOut = projectOut system;
340369 serverOn = projectInst: version: serverExe
341370 projectInst.ghc.backend
@@ -351,7 +380,7 @@ in rec {
351380 exe = serverOn mainProjectOut dummyVersion;
352381 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
353382 server (args // { exe = linuxExe version; });
354− obelisk = import (base + "/.obelisk/impl") {};
383+ obelisk = import (base' + "/.obelisk/impl") {};
355384 };
356385 haskellPackageSets = {
357386 inherit (reflex-platform) ghc ghcjs;
lib/asset/serve-snap/src/Obelisk/Asset/Accept.hs+6−6
@@ -136,16 +136,16 @@ qvalue = do
136136 skipMany lws
137137 q0 <|> q1
138138 where q0 = do
139− char '0'
139+ _ <- char '0'
140140 decimals <- option [] $ do
141− char '.'
141+ _ <- char '.'
142142 starRule (Just 0) (Just numAllowedDigits) digit
143143 return $ QValue $ MkFixed $ fromIntegral $ (read ('0' : decimals) :: Int) * 10 ^ (numAllowedDigits - length decimals)
144144 q1 = do
145− char '1'
145+ _ <- char '1'
146146 option () $ do
147− char '.'
148− starRule (Just 0) (Just numAllowedDigits) $ char '0'
147+ _ <- char '.'
148+ _ <- starRule (Just 0) (Just numAllowedDigits) $ char '0'
149149 return ()
150150 return $ QValue 1
151151 numAllowedDigits :: Int
@@ -232,7 +232,7 @@ hashRule minNum maxNum element = do
232232lws :: Parser ()
233233lws = do
234234 option () $ cr >> lf
235− starRule (Just 1) Nothing $ sp <|> ht
235+ _ <- starRule (Just 1) Nothing $ sp <|> ht
236236 return ()
237237
238238-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
lib/backend/src/Obelisk/Backend.hs−2
@@ -27,7 +27,6 @@ import Control.Category
2727import Control.Monad
2828import Control.Monad.Except
2929import Control.Monad.Fail (MonadFail)
30−import Control.Categorical.Bifunctor
3130import Data.ByteString (ByteString)
3231import qualified Data.ByteString.Char8 as BSC8
3332import Data.Default (Default (..))
@@ -38,7 +37,6 @@ import qualified Data.Map as Map
3837import Data.Monoid ((<>))
3938import Data.Text (Text)
4039import qualified Data.Text as T
41−import Data.Text.Encoding
4240import Obelisk.Asset.Serve.Snap (serveAsset)
4341import qualified Obelisk.ExecutableConfig.Lookup as Lookup
4442import Obelisk.Frontend
lib/cliapp/obelisk-cliapp.cabal+2−1
@@ -7,7 +7,8 @@ cabal-version: >=1.2
77library
88 hs-source-dirs: src
99 build-depends:
10− ansi-terminal
10+ aeson
11+ , ansi-terminal
1112 , base
1213 , bytestring
1314 , containers
lib/cliapp/src/Obelisk/CliApp.hs+1
@@ -45,6 +45,7 @@ module Obelisk.CliApp
4545 , readCreateProcessWithExitCode
4646 , readProcessAndLogOutput
4747 , readProcessAndLogStderr
48+ , readProcessJSONAndLogStderr
4849 , reconstructCommand
4950 , setCwd
5051 , setDelegateCtlc
lib/cliapp/src/Obelisk/CliApp/Logging.hs+11−10
@@ -39,7 +39,6 @@ import Control.Monad.Log (Severity (..), WithSeverity (..), logMessage, runLoggi
3939import Control.Monad.Loops (iterateUntil)
4040import Control.Monad.Reader (MonadIO, ReaderT (..))
4141import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)
42−import Data.Maybe
4342import Data.Semigroup ((<>))
4443import Data.Text (Text)
4544import qualified Data.Text as T
@@ -67,7 +66,7 @@ newCliConfig sev noColor noSpinner errorLogExitCode = do
6766 tipDisplayed <- newIORef False
6867 stack <- newIORef ([], [])
6968 textEncoding <- hGetEncoding stdout
70− let theme = if fromMaybe False $ supportsUnicode <$> textEncoding
69+ let theme = if maybe False supportsUnicode textEncoding
7170 then unicodeTheme
7271 else noUnicodeTheme
7372 return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode theme
@@ -133,7 +132,7 @@ handleLog' noColor output = do
133132 hFlush stdout
134133 Output_Overwrite ts -> liftIO $ do
135134 width <- TS.getTerminalWidth
136− T.putStr $ "\r" <> (TS.render (not noColor) width ts)
135+ T.putStr $ "\r" <> TS.render (not noColor) width ts
137136 hFlush stdout
138137 Output_ClearLine -> liftIO $ do
139138 -- Go to the first column and clear the whole line
@@ -179,14 +178,16 @@ withExitFailMessage msg f = f `catch` \(e :: ExitCode) -> do
179178
180179-- | Write log to stdout, with colors (unless `noColor`)
181180writeLog :: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
182−writeLog withNewLine noColor (WithSeverity severity s)
183− | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
184− | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
185− | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
186− | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
187− | otherwise = liftIO $ putFn s
181+writeLog withNewLine noColor (WithSeverity severity s) = if T.null s then pure () else write
188182 where
189− putFn = if withNewLine then (T.hPutStrLn h) else (T.hPutStr h)
183+ write
184+ | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
185+ | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
186+ | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
187+ | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
188+ | otherwise = liftIO $ putFn s
189+
190+ putFn = if withNewLine then T.hPutStrLn h else T.hPutStr h
190191 h = if severity <= Error then stderr else stdout
191192 errorColors = [SetColor Foreground Vivid Red]
192193 warningColors = [SetColor Foreground Vivid Yellow]
lib/cliapp/src/Obelisk/CliApp/Process.hs+15
@@ -22,6 +22,7 @@ module Obelisk.CliApp.Process
2222 , readCreateProcessWithExitCode
2323 , readProcessAndLogOutput
2424 , readProcessAndLogStderr
25+ , readProcessJSONAndLogStderr
2526 , reconstructCommand
2627 , setCwd
2728 , setDelegateCtlc
@@ -52,6 +53,7 @@ import qualified System.IO.Streams as Streams
5253import System.IO.Streams.Concurrent (concurrentMerge)
5354import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out, waitForProcess)
5455import qualified System.Process as Process
56+import qualified Data.Aeson as Aeson
5557
5658import Control.Monad.Log (Severity (..))
5759import Obelisk.CliApp.Logging (putLog, putLogRaw)
@@ -101,6 +103,19 @@ readProcessAndLogStderr sev process = do
101103 streamToLog =<< liftIO (streamHandle sev err)
102104 liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
103105
106+readProcessJSONAndLogStderr
107+ :: (Aeson.FromJSON a, MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
108+ => Severity -> ProcessSpec -> m a
109+readProcessJSONAndLogStderr sev process = do
110+ (out, _err) <- withProcess process $ \_out err -> do
111+ streamToLog =<< liftIO (streamHandle sev err)
112+ json <- liftIO $ BS.hGetContents out
113+ case Aeson.eitherDecodeStrict json of
114+ Right a -> pure a
115+ Left err -> do
116+ putLog Error $ "Could not decode process output as JSON: " <> T.pack err
117+ throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) 0
118+
104119readCreateProcessWithExitCode
105120 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
106121 => ProcessSpec -> m (ExitCode, String, String)
lib/command/obelisk-command.cabal+9−7
@@ -7,35 +7,36 @@ library
77 hs-source-dirs: src
88 build-depends:
99 aeson
10− , ansi-terminal
11− , Cabal
1210 , aeson-pretty
11+ , ansi-terminal
1312 , base
1413 , base16-bytestring
1514 , binary
1615 , bytestring
17− , obelisk-cliapp
16+ , Cabal
1817 , containers
1918 , data-default
2019 , directory
2120 , either
2221 , errors
22+ , exceptions
23+ , extra
2324 , filepath
25+ , git
2426 , github
2527 , here
26− , git
2728 , hnix
2829 , hpack
2930 , io-streams
30− , exceptions
31− , logging-effect
3231 , lens
32+ , logging-effect
33+ , megaparsec
3334 , modern-uri
3435 , monad-loops
3536 , mtl
36− , megaparsec
3737 , network
3838 , network-uri
39+ , obelisk-cliapp
3940 , optparse-applicative
4041 , placeholders
4142 , process
@@ -58,6 +59,7 @@ library
5859 Obelisk.Command.Thunk
5960 Obelisk.Command.Utils
6061 Obelisk.Command.VmBuilder
62+ Obelisk.Command.Preprocessor
6163 -- -fobject-code is so that the StaticPointers extension can work in ghci
6264 ghc-options: -Wall -fobject-code
6365
lib/command/src/Obelisk/Command.hs+14−25
@@ -15,7 +15,6 @@ import qualified Data.ByteString.Base16 as Base16
1515import qualified Data.ByteString.Lazy as LBS
1616import Data.Foldable (for_)
1717import Data.List
18−import Data.Maybe (catMaybes)
1918import qualified Data.Text as T
2019import Data.Text.Encoding
2120import Data.Text.Encoding.Error (lenientDecode)
@@ -36,8 +35,8 @@ import Obelisk.Command.Deploy
3635import Obelisk.Command.Project
3736import Obelisk.Command.Run
3837import Obelisk.Command.Thunk
39−import Obelisk.Command.Utils
4038import qualified Obelisk.Command.VmBuilder as VmBuilder
39+import qualified Obelisk.Command.Preprocessor as Preprocessor
4140
4241
4342data Args = Args
@@ -102,26 +101,11 @@ data ObCommand
102101data ObInternal
103102 = ObInternal_RunStaticIO StaticKey
104103 | ObInternal_CLIDemo
104+ -- the preprocessor argument syntax is also handled outside
105+ -- optparse-applicative, but it shouldn't ever conflict with another syntax
106+ | ObInternal_ApplyPackages String String String [String]
105107 deriving Show
106108
107−inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
108−inNixShell' p = withProjectRoot "." $ \root -> do
109− cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
110− projectShell root True "ghc" (Just cmd)
111− where
112− mkCmd = do
113− argsCfg <- getArgsConfig
114− myArgs <- getArgs
115− obArgs <- parseCLIArgs argsCfg myArgs
116− progName <- getObeliskExe
117− return $ progName : catMaybes
118− [ Just "--no-handoff"
119− , bool Nothing (Just "--verbose") $ _args_verbose obArgs
120− , Just "internal"
121− , Just "run-static-io"
122− , Just $ encodeStaticKey $ staticKey p
123− ]
124−
125109obCommand :: ArgsConfig -> Parser ObCommand
126110obCommand cfg = hsubparser
127111 (mconcat
@@ -276,7 +260,7 @@ parserPrefs = defaultPrefs
276260 }
277261
278262parseCLIArgs :: ArgsConfig -> [String] -> IO Args
279−parseCLIArgs cfg as = pure as >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
263+parseCLIArgs cfg = handleParseResult . execParserPure parserPrefs (argsInfo cfg)
280264
281265-- | Create an Obelisk config for the current process.
282266mkObeliskConfig :: IO Obelisk
@@ -349,6 +333,9 @@ main' argsCfg = do
349333 liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
350334 case myArgs of
351335 "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
336+ origPath:inPath:outPath:preprocessorName:packagePaths
337+ | preprocessorName == preprocessorIdentifier && any (\c -> c == '.' || c == pathSeparator) origPath ->
338+ ob $ ObCommand_Internal $ ObInternal_ApplyPackages origPath inPath outPath packagePaths
352339 a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
353340 | "--bash-completion" `isPrefixOf` a
354341 && "--no-handoff" `elem` as -> go (a:as)
@@ -389,24 +376,26 @@ ob = \case
389376 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
390377 DeployCommand_Update -> deployUpdate "."
391378 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
392− ObCommand_Run -> inNixShell' $ static run
379+ ObCommand_Run -> run
393380 ObCommand_Thunk tc -> case tc of
394381 ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
395382 ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
396383 ThunkCommand_Pack thunks config -> for_ thunks (packThunk config)
397384 ObCommand_Repl -> runRepl
398− ObCommand_Watch -> inNixShell' $ static runWatch
385+ ObCommand_Watch -> runWatch
399386 ObCommand_Shell so -> withProjectRoot "." $ \root ->
400387 projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
401388 ObCommand_Doc shell' pkgs -> withProjectRoot "." $ \root ->
402389 projectShell root False shell' (Just $ haddockCommand pkgs)
403390 ObCommand_Internal icmd -> case icmd of
404− ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
391+ ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case -- TODO: DELETE
405392 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
406393 Just p -> do
407394 c <- getObelisk
408395 liftIO $ runObelisk c $ deRefStaticPtr p
409− ObInternal_CLIDemo -> cliDemo
396+ ObInternal_CLIDemo -> cliDemo -- TODO: DELETE
397+ ObInternal_ApplyPackages origPath inPath outPath packagePaths -> do
398+ liftIO $ Preprocessor.applyPackages origPath inPath outPath packagePaths
410399
411400haddockCommand :: [String] -> String
412401haddockCommand pkgs = unwords
lib/command/src/Obelisk/Command/Nix.hs+44−13
@@ -9,22 +9,31 @@
99{-# LANGUAGE TemplateHaskell #-}
1010{-# LANGUAGE TypeApplications #-}
1111module Obelisk.Command.Nix
12− ( nixCmd
13− , NixCmd (..)
14− , nixCmdConfig_target
15− , nixCmdConfig_args
16− , nixCmdConfig_builders
12+ ( Arg (..)
1713 , NixBuildConfig (..)
1814 , nixBuildConfig_common
1915 , nixBuildConfig_outLink
16+ , NixCmd (..)
17+ , nixCmdConfig_args
18+ , nixCmdConfig_builders
19+ , nixCmdConfig_target
20+ , NixCommonConfig (..)
2021 , NixInstantiateConfig (..)
2122 , nixInstantiateConfig_eval
22− , NixCommonConfig (..)
23− , Target (..)
23+ , NixShellConfig (..)
24+ , nixShellConfig_common
25+ , nixShellConfig_pure
26+ , nixShellConfig_run
2427 , OutLink (..)
25− , Arg (..)
28+ , Target (..)
29+ , target_attr
30+ , target_expr
31+ , target_path
32+
2633 , boolArg
34+ , nixCmd
2735 , rawArg
36+ , runNixShellConfig
2837 , strArg
2938 ) where
3039
@@ -62,10 +71,10 @@ data Arg
6271 deriving (Eq, Show)
6372
6473strArg :: String -> String -> Arg
65−strArg k = Arg_Str k
74+strArg = Arg_Str
6675
6776rawArg :: String -> String -> Arg
68−rawArg k = Arg_Expr k
77+rawArg = Arg_Expr
6978
7079boolArg :: String -> Bool -> Arg
7180boolArg k = Arg_Expr k . bool "false" "true"
@@ -85,7 +94,7 @@ makeClassy ''NixCommonConfig
8594instance Default NixCommonConfig where
8695 def = NixCommonConfig def mempty mempty
8796
88−runNixCommonConfig :: NixCommonConfig -> [FilePath]
97+runNixCommonConfig :: NixCommonConfig -> [String]
8998runNixCommonConfig cfg = mconcat [maybeToList path, attrArg, exprArg, args, buildersArg]
9099 where
91100 path = _target_path $ _nixCmdConfig_target cfg
@@ -123,7 +132,7 @@ instance HasNixCommonConfig NixBuildConfig where
123132instance Default NixBuildConfig where
124133 def = NixBuildConfig def def
125134
126−runNixBuildConfig :: NixBuildConfig -> [FilePath]
135+runNixBuildConfig :: NixBuildConfig -> [String]
127136runNixBuildConfig cfg = mconcat
128137 [ runNixCommonConfig $ cfg ^. nixCommonConfig
129138 , case _nixBuildConfig_outLink cfg of
@@ -144,12 +153,26 @@ instance HasNixCommonConfig NixInstantiateConfig where
144153instance Default NixInstantiateConfig where
145154 def = NixInstantiateConfig def False
146155
147−runNixInstantiateConfig :: NixInstantiateConfig -> [FilePath]
156+runNixInstantiateConfig :: NixInstantiateConfig -> [String]
148157runNixInstantiateConfig cfg = mconcat
149158 [ runNixCommonConfig $ cfg ^. nixCommonConfig
150159 , "--eval" <$ guard (_nixInstantiateConfig_eval cfg)
151160 ]
152161
162+data NixShellConfig = NixShellConfig
163+ { _nixShellConfig_common :: NixCommonConfig
164+ , _nixShellConfig_pure :: Bool
165+ , _nixShellConfig_run :: Maybe String
166+ }
167+
168+makeLenses ''NixShellConfig
169+
170+instance HasNixCommonConfig NixShellConfig where
171+ nixCommonConfig = nixShellConfig_common
172+
173+instance Default NixShellConfig where
174+ def = NixShellConfig def False Nothing
175+
153176data NixCmd
154177 = NixCmd_Build NixBuildConfig
155178 | NixCmd_Instantiate NixInstantiateConfig
@@ -157,6 +180,14 @@ data NixCmd
157180instance Default NixCmd where
158181 def = NixCmd_Build def
159182
183+runNixShellConfig :: NixShellConfig -> [String]
184+runNixShellConfig cfg = mconcat
185+ [ runNixCommonConfig $ cfg ^. nixCommonConfig
186+ , [ "--pure" | cfg ^. nixShellConfig_pure ]
187+ ] ++ mconcat [
188+ ["--run", run] | run <- maybeToList $ cfg ^. nixShellConfig_run
189+ ]
190+
160191nixCmd :: MonadObelisk m => NixCmd -> m FilePath
161192nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
162193 output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) options
lib/command/src/Obelisk/Command/Preprocessor.hsnew file+93
@@ -0,0 +1,93 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+module Obelisk.Command.Preprocessor where
5+
6+import qualified Data.ByteString.Lazy as BL
7+import qualified Data.ByteString.Lazy.Builder as BU
8+import Data.List (intersperse, isPrefixOf, sortOn)
9+import Data.Maybe (fromMaybe)
10+import qualified Data.Text.Lazy.Builder as TL
11+import qualified Data.Text.Lazy.Encoding as TL
12+import Distribution.Compiler (CompilerFlavor (..))
13+import Language.Haskell.Extension (Extension (..), Language(..))
14+import System.IO (IOMode (..), hClose, hPutStrLn, openFile, stderr)
15+import System.FilePath (hasTrailingPathSeparator, joinPath, normalise, splitPath)
16+
17+import Obelisk.Command.Run (CabalPackageInfo (..), parseCabalPackage')
18+
19+applyPackages :: FilePath -> FilePath -> FilePath -> [FilePath] -> IO ()
20+applyPackages origPath inPath outPath packagePaths = do
21+ -- This code is intended to be executed via ghci's -pgmF preprocessor option
22+ -- The command line arguments are passed in via ghc, which dictates the first three options and meanings
23+ -- In order for this code to execute, origPath must contain either a '.' character or a '/' character.
24+ -- (This is to avoid the possibility of the command line syntax conflicting with another ob command)
25+ -- We do have control over the remaining arguments, but they must be the same for all files.
26+ -- Thus, the fourth command line argument must be "apply-packages", which has already been handled.
27+ -- We assume all the remaining arguments passed in are paths to cabal or hpack package specifications.
28+
29+ outFile <- openFile outPath WriteMode
30+ let hPutTextBuilder h = BU.hPutBuilder h . TL.encodeUtf8Builder . TL.toLazyText
31+
32+ -- putStr "--------------------------------------------------------------------------------\n"
33+ -- print args
34+ -- putStr "--------------------------------------------------------------------------------\n"
35+
36+ -- Thus we must select among the packagePaths for the file we are going to parse.
37+
38+ let takeDirs = takeWhile hasTrailingPathSeparator
39+ packageDirs = sortOn (negate . length . takeDirs) $ map (splitPath . normalise) packagePaths
40+ origDir = splitPath $ normalise origPath
41+ matches = [ joinPath d | d <- packageDirs, takeDirs d `isPrefixOf` origDir ]
42+
43+ -- So the first element of matches is going to be the deepest path to a package spec that contains
44+ -- our file as a subdirectory.
45+
46+ case matches of
47+ [] -> hPutTextBuilder outFile (lineNumberPragma origPath) -- TODO: probably should produce a warning
48+ (packagePath:_) -> do
49+ parseCabalPackage' packagePath >>= \case
50+ Left err ->
51+ hPutStrLn stderr $ "Error: Unable to parse cabal package " <> packagePath <> "; Skipping preprocessor on " <> origPath <> ". Error was " <> show err
52+ Right (_warnings, packageInfo) ->
53+ hPutTextBuilder outFile (generateHeader origPath packageInfo)
54+
55+ BL.readFile inPath >>= BL.hPut outFile
56+ hClose outFile
57+
58+
59+ -- I'm pretty sure there's a certain amount of oversimplification in CabalPackageInfo, so I doubt this is fully robust.
60+
61+generateHeader :: FilePath -> CabalPackageInfo -> TL.Builder
62+generateHeader origPath packageInfo =
63+ hsExtensions <> ghcOptions <> lineNumberPragma origPath
64+ where
65+ hsExtensions =
66+ if not (null extList)
67+ then TL.fromText "{-# LANGUAGE "
68+ <> mconcat (intersperse (TL.fromText ", ") extList)
69+ <> TL.fromText " #-}\n"
70+ else mempty
71+ extList = addDefaultLanguage $ concatMap showExt $ _cabalPackageInfo_defaultExtensions packageInfo
72+ addDefaultLanguage =
73+ case _cabalPackageInfo_defaultLanguage packageInfo of
74+ Nothing -> id
75+ Just x -> case x of
76+ UnknownLanguage ext -> ( TL.fromString ext :)
77+ ext -> ( TL.fromString (show ext) :)
78+ showExt = \case
79+ EnableExtension ext -> [TL.fromString (show ext)]
80+ DisableExtension _ -> []
81+ UnknownExtension ext -> [TL.fromString ext]
82+
83+ ghcOptions =
84+ if not (null optList)
85+ then TL.fromText "{-# OPTIONS_GHC "
86+ <> mconcat (intersperse (TL.fromText " ") optList)
87+ <> TL.fromText " #-}\n"
88+ else mempty
89+ optList = map TL.fromString $ fromMaybe [] $ lookup GHC (_cabalPackageInfo_compilerOptions packageInfo)
90+
91+lineNumberPragma :: FilePath -> TL.Builder
92+lineNumberPragma origPath =
93+ TL.fromText "{-# LINE 1 \"" <> TL.fromString origPath <> TL.fromText "\" #-}\n"
lib/command/src/Obelisk/Command/Project.hs+55−19
@@ -1,28 +1,36 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE LambdaCase #-}
33{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE TemplateHaskell #-}
54module Obelisk.Command.Project
65 ( InitSource (..)
7− , initProject
86 , findProjectObeliskCommand
97 , findProjectRoot
10− , withProjectRoot
11− , inProjectShell
12− , inImpureProjectShell
8+ , initProject
9+ , nixShellWithPkgs
10+ , obeliskDirName
1311 , projectShell
14−
15− , toObeliskDir
1612 , toImplDir
13+ , toNixPath
14+ , toObeliskDir
15+ , withProjectRoot
1716 ) where
1817
1918import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
19+import Control.Lens ((.~), (?~), (<&>))
2020import Control.Monad
2121import Control.Monad.IO.Class (liftIO)
2222import Control.Monad.State
23+import qualified Data.Aeson as Json
2324import Data.Bits
24−import Data.Function (on)
25+import qualified Data.ByteString.Lazy as BSL
26+import Data.Default (def)
27+import Data.Function ((&), on)
28+import Data.List (isInfixOf)
29+import Data.Map (Map)
30+import Data.Text (Text)
2531import qualified Data.Text as T
32+import Data.Text.Encoding (decodeUtf8)
33+import Data.Traversable (for)
2634import System.Directory
2735import System.Environment (lookupEnv)
2836import System.FilePath
@@ -31,14 +39,15 @@ import System.IO.Unsafe (unsafePerformIO)
3139import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
3240import System.Posix.Files
3341import System.Process (waitForProcess)
34−import System.Which (staticWhich)
3542
3643import GitHub.Data.GitData (Branch)
3744import GitHub.Data.Name (Name)
3845
3946import Obelisk.App (MonadObelisk)
4047import Obelisk.CliApp
48+import Obelisk.Command.Nix
4149import Obelisk.Command.Thunk
50+import Obelisk.Command.Utils (nixExePath)
4251
4352--TODO: Make this module resilient to random exceptions
4453
@@ -62,9 +71,12 @@ data InitSource
6271 | InitSource_Symlink FilePath
6372 deriving Show
6473
74+obeliskDirName :: FilePath
75+obeliskDirName = ".obelisk"
76+
6577-- | Path to obelisk directory in given path
6678toObeliskDir :: FilePath -> FilePath
67−toObeliskDir p = p </> ".obelisk"
79+toObeliskDir p = p </> obeliskDirName
6880
6981-- | Path to impl file in given path
7082toImplDir :: FilePath -> FilePath
@@ -234,19 +246,43 @@ filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= uma
234246 fileGroupWritable = fileMode s .&. 0o020 == 0o020
235247 umaskGroupWritable = umask .&. 0o020 == 0
236248
237−-- | Run a command in the given shell for the current project
238−inProjectShell :: MonadObelisk m => String -> String -> m ()
239−inProjectShell shellName command = withProjectRoot "." $ \root ->
240− projectShell root True shellName (Just command)
249+-- | Nix syntax requires relative paths to be prefixed by @./@ or
250+-- @../@. This will make a 'FilePath' that can be embedded in a Nix
251+-- expression.
252+toNixPath :: FilePath -> FilePath
253+toNixPath root | "/" `isInfixOf` root = root
254+ | otherwise = "./" <> root
241255
242−inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
243−inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
244− projectShell root False shellName (Just command)
256+nixShellWithPkgs :: MonadObelisk m => FilePath -> Bool -> Map Text FilePath -> Maybe String -> m ()
257+nixShellWithPkgs root isPure packageNamesAndPaths command = do
258+ packageNamesAndAbsPaths <- liftIO $ for packageNamesAndPaths makeAbsolute
259+ nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
260+ proc nixExePath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
261+ nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
262+ (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setDelegateCtlc True $ setCwd (Just root) $ proc "nix-shell" $
263+ runNixShellConfig $ def
264+ & nixShellConfig_pure .~ isPure
265+ & nixShellConfig_common . nixCmdConfig_target .~ (def
266+ & target_path .~ Nothing
267+ & target_expr ?~
268+ "{root, pkgs}: ((import root {}).passthru.__unstable__.self.extend (self: super: {\
269+ \shellPackages = builtins.fromJSON pkgs; })\
270+ \).project.shells.ghc"
271+ )
272+ & nixShellConfig_common . nixCmdConfig_args .~
273+ [ rawArg "root" $ toNixPath root
274+ , strArg "pkgs" (T.unpack $ decodeUtf8 $ BSL.toStrict $ Json.encode packageNamesAndAbsPaths)
275+ ]
276+ & nixShellConfig_run .~ (command <&> \c -> mconcat
277+ [ "export NIX_PATH=nixpkgs=", T.unpack nixpkgsPath, "; "
278+ , maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote
279+ , c
280+ ])
281+ void $ liftIO $ waitForProcess ph
245282
246283projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
247284projectShell root isPure shellName command = do
248− let nixPath = $(staticWhich "nix")
249− nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ proc nixPath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
285+ nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ proc nixExePath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
250286 nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
251287 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setDelegateCtlc True $ setCwd (Just root) $ proc "nix-shell" $
252288 [ "default.nix"] <>
lib/command/src/Obelisk/Command/Run.hs+174−105
@@ -3,47 +3,58 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TemplateHaskell #-}
76{-# LANGUAGE TupleSections #-}
87module Obelisk.Command.Run where
98
10−import Control.Applicative (liftA2)
9+import Control.Arrow ((&&&))
1110import Control.Exception (Exception, bracket)
12−import Control.Monad
11+import Control.Monad (filterM, unless)
1312import Control.Monad.Except (runExceptT, throwError)
1413import Control.Monad.IO.Class (liftIO)
1514import Control.Monad.Reader (MonadIO)
15+import Data.Coerce (coerce)
1616import Data.Either
17−import Data.Foldable (for_)
18−import qualified Data.List as L
17+import Data.Foldable (for_, toList)
18+import Data.List.Extra (dropPrefix)
1919import qualified Data.List.NonEmpty as NE
20+import Data.Map (Map)
21+import qualified Data.Map as Map
2022import Data.Maybe
23+import qualified Data.Set as Set
24+import Data.Text (Text)
2125import qualified Data.Text as T
26+import Data.Traversable (for)
27+import Debug.Trace (trace)
2228import Distribution.Compiler (CompilerFlavor(..))
2329import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
2430import Distribution.Parsec.ParseResult (runParseResult)
25−import Distribution.Pretty
31+import qualified Distribution.System as Dist
2632import Distribution.Types.BuildInfo
2733import Distribution.Types.CondTree
2834import Distribution.Types.GenericPackageDescription
2935import Distribution.Types.Library
3036import Distribution.Utils.Generic
37+import qualified Distribution.Parsec.Common as Dist
3138import qualified Hpack.Config as Hpack
3239import qualified Hpack.Render as Hpack
3340import qualified Hpack.Yaml as Hpack
3441import Language.Haskell.Extension
3542import Network.Socket hiding (Debug)
3643import System.Directory
44+import System.Environment (getExecutablePath)
3745import System.FilePath
46+import qualified System.Info
3847import System.IO.Temp (withSystemTempDirectory)
39−import System.Which (staticWhich)
4048
41−import Obelisk.App (MonadObelisk, ObeliskT)
42−import Obelisk.CliApp (Severity (..) , callCommand, failWith, putLog, proc, readProcessAndLogStderr)
43−import Obelisk.Command.Project (inProjectShell, withProjectRoot)
49+import Obelisk.App (MonadObelisk)
50+import Obelisk.CliApp (Severity (..) , failWith, putLog, proc, readCreateProcessWithExitCode, readProcessAndLogStderr)
51+import Obelisk.Command.Project (obeliskDirName, toObeliskDir, withProjectRoot, nixShellWithPkgs, toNixPath)
52+import Obelisk.Command.Utils (findExePath, ghcidExePath, nixBuildExePath, nixExePath)
4453
4554data CabalPackageInfo = CabalPackageInfo
46− { _cabalPackageInfo_packageRoot :: FilePath
55+ { _cabalPackageInfo_packageFile :: FilePath
56+ , _cabalPackageInfo_packageName :: T.Text
57+ , _cabalPackageInfo_packageRoot :: FilePath
4758 , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
4859 -- ^ List of hs src dirs of the library component
4960 , _cabalPackageInfo_defaultExtensions :: [Extension]
@@ -54,20 +65,19 @@ data CabalPackageInfo = CabalPackageInfo
5465 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
5566 }
5667
57−-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
58−run :: ObeliskT IO ()
59−run = do
60− let nixPath = $(staticWhich "nix")
61− nixBuildPath = $(staticWhich "nix-build")
62− pkgs <- getLocalPkgs
68+-- | Used to signal to obelisk that it's being invoked as a preprocessor
69+preprocessorIdentifier :: String
70+preprocessorIdentifier = "__preprocessor-apply-packages"
71+
72+run :: MonadObelisk m => m ()
73+run = withProjectRoot "." $ \root -> do
74+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
6375 withGhciScript pkgs $ \dotGhciPath -> do
6476 freePort <- getFreePort
65− assets <- withProjectRoot "." $ \root -> do
66− let importableRoot = if "/" `isInfixOf` root
67− then root
68− else "./" <> root
77+ assets <- do
78+ let importableRoot = toNixPath root
6979 isDerivation <- readProcessAndLogStderr Debug $
70− proc nixPath
80+ proc nixExePath
7181 [ "eval"
7282 , "-f"
7383 , root
@@ -79,83 +89,136 @@ run = do
7989 -- Check whether the impure static files are a derivation (and so must be built)
8090 if isDerivation == "1"
8191 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
82− proc nixBuildPath
92+ proc nixBuildExePath
8393 [ "--no-out-link"
8494 , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
8595 ]
8696 else readProcessAndLogStderr Debug $
87− proc nixPath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
97+ proc nixExePath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
8898 putLog Debug $ "Assets impurely loaded from: " <> assets
89− runGhcid dotGhciPath $ Just $ unwords
99+ runGhcid dotGhciPath pkgs $ Just $ unwords
90100 [ "Obelisk.Run.run"
91101 , show freePort
92− , "(runServeAsset " ++ show assets ++ ")"
102+ , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
93103 , "Backend.backend"
94104 , "Frontend.frontend"
95105 ]
96106
97107runRepl :: MonadObelisk m => m ()
98108runRepl = do
99− pkgs <- getLocalPkgs
109+ pkgs <- fmap toList . parsePackagesOrFail =<< withProjectRoot "." getLocalPkgs
100110 withGhciScript pkgs $ \dotGhciPath -> do
101− runGhciRepl dotGhciPath
111+ runGhciRepl pkgs dotGhciPath
102112
103113runWatch :: MonadObelisk m => m ()
104114runWatch = do
105− pkgs <- getLocalPkgs
106− withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
115+ pkgs <- fmap toList . parsePackagesOrFail =<< withProjectRoot "." getLocalPkgs
116+ withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath pkgs Nothing
117+
118+-- | Relative paths to local packages of an obelisk project.
119+--
120+-- These are a combination of the obelisk predefined local packages,
121+-- and any packages that the user has set with the @packages@ argument
122+-- to the Nix @project@ function.
123+getLocalPkgs :: MonadObelisk m => FilePath -> m [FilePath]
124+getLocalPkgs root = do
125+ (_exitCode, out, err) <- readCreateProcessWithExitCode $
126+ proc findExePath ["-L", root, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
127+ putLog Debug $ T.strip $ T.pack err
128+
129+ let
130+ -- We ignore any path that has ".obelisk" in it, but keep the root ".obelisk" paths
131+ packagePaths = filter (not . isIgnored) $ map T.unpack $ T.lines $ T.strip $ T.pack out
132+ isIgnored path = obeliskDirName `elem` dropPrefix (splitPath $ toObeliskDir root) (splitPath path)
133+ pure packagePaths
107134
108135data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
109136 deriving (Eq, Ord, Show)
110137instance Exception GuessPackageFileError
111138
139+newtype HPackFilePath = HPackFilePath { unHPackFilePath :: FilePath } deriving (Eq, Ord, Show)
140+newtype CabalFilePath = CabalFilePath { unCabalFilePath :: FilePath } deriving (Eq, Ord, Show)
141+
112142-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
113143guessCabalPackageFile
114− :: MonadIO m
115− => FilePath -- ^ Directory to search for cabal package
116− -> m (Either GuessPackageFileError (Either FilePath FilePath))
117− -- ^ 'Right' 'Left' for hpack package, 'Right' 'Right' for cabal package; 'FilePath' is relative to given directory.
118−guessCabalPackageFile dir = do
119− candidates <- liftIO $
120− filterM (doesFileExist . (dir </>))
121− =<< filter (liftA2 (||) (== Hpack.packageConfig) (".cabal" `L.isSuffixOf`))
122− <$> listDirectory dir
123− pure $ case L.partition (== Hpack.packageConfig) candidates of
124− ([hpack], _) -> Right $ Left hpack
125− ([], [cabal]) -> Right $ Right cabal
126− ([], []) -> Left GuessPackageFileError_NotFound
127− (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ hpacks <> cabals
144+ :: (MonadIO m)
145+ => FilePath -- ^ Directory or path to search for cabal package
146+ -> m (Either GuessPackageFileError (Either CabalFilePath HPackFilePath))
147+guessCabalPackageFile pkg = do
148+ liftIO (doesDirectoryExist pkg) >>= \case
149+ False -> case cabalOrHpackFile pkg of
150+ (Just hpack@(Right _)) -> pure $ Right hpack
151+ (Just cabal@(Left (CabalFilePath cabalFilePath))) -> do
152+ -- If the cabal file has a sibling hpack file, we use that instead
153+ -- since running hpack often generates a sibling cabal file
154+ let possibleHpackSibling = takeDirectory cabalFilePath </> Hpack.packageConfig
155+ hasHpackSibling <- liftIO $ doesFileExist possibleHpackSibling
156+ pure $ Right $ if hasHpackSibling then Right (HPackFilePath possibleHpackSibling) else cabal
157+ Nothing -> pure $ Left GuessPackageFileError_NotFound
158+ True -> do
159+ candidates <- liftIO $
160+ filterM (doesFileExist . either unCabalFilePath unHPackFilePath)
161+ =<< mapMaybe (cabalOrHpackFile . (pkg </>)) <$> listDirectory pkg
162+ pure $ case partitionEithers candidates of
163+ ([hpack], _) -> Right $ Left hpack
164+ ([], [cabal]) -> Right $ Right cabal
165+ ([], []) -> Left GuessPackageFileError_NotFound
166+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ coerce hpacks <> coerce cabals
128167
129−-- | Relative paths to local packages of an obelisk project
130−-- TODO a way to query this
131−getLocalPkgs :: Applicative f => f [FilePath]
132−getLocalPkgs = pure ["backend", "common", "frontend"]
168+cabalOrHpackFile :: FilePath -> Maybe (Either CabalFilePath HPackFilePath)
169+cabalOrHpackFile = \case
170+ x | takeExtension x == ".cabal" -> Just (Left $ CabalFilePath x)
171+ | takeFileName x == Hpack.packageConfig -> Just (Right $ HPackFilePath x)
172+ | otherwise -> Nothing
133173
174+-- | Parses the cabal package in a given directory.
175+-- This automatically figures out which .cabal file or package.yaml (hpack) file to use in the given directory.
134176parseCabalPackage
135− :: (MonadObelisk m)
136− => FilePath -- ^ package directory
177+ :: MonadObelisk m
178+ => FilePath -- ^ Package directory
137179 -> m (Maybe CabalPackageInfo)
138−parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< runExceptT $ do
139− (cabalContents, packageFile) <- guessCabalPackageFile dir >>= \case
140− Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack dir
141− Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack dir
142− Right (Right cabalFileName) -> let file = dir </> cabalFileName in (, file) <$> liftIO (readUTF8File file)
143− Right (Left hpackFileName) -> do
180+parseCabalPackage dir = parseCabalPackage' dir >>= \case
181+ Left err -> Nothing <$ putLog Error err
182+ Right (warnings, pkgInfo) -> do
183+ for_ warnings $ putLog Warning . T.pack . show
184+ pure $ Just pkgInfo
185+
186+-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
187+parseCabalPackage'
188+ :: (MonadIO m)
189+ => FilePath -- ^ Package directory
190+ -> m (Either T.Text ([Dist.PWarning], CabalPackageInfo))
191+parseCabalPackage' pkg = runExceptT $ do
192+ (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
193+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
194+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack pkg
195+ Right (Left (CabalFilePath file)) -> (, file, takeBaseName file) <$> liftIO (readUTF8File file)
196+ Right (Right (HPackFilePath file)) -> do
144197 let
145− file = dir </> hpackFileName
146198 decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
147199 liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
148200 Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
149− Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file)
201+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file, Hpack.packageName hpackPackage)
150202
151− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
152− toUTF8BS cabalContents
153− for_ warnings $ putLog Warning . T.pack . show
203+ let
204+ (warnings, result) = runParseResult $ parseGenericPackageDescription $ toUTF8BS cabalContents
205+ osConfVar = case System.Info.os of
206+ "linux" -> Just Dist.Linux
207+ "darwin" -> Just Dist.OSX
208+ _ -> trace "Unrecgonized System.Info.os" Nothing
209+ archConfVar = Just Dist.X86_64 -- TODO: Actually infer this
210+ evalConfVar v = Right $ case v of
211+ OS osVar -> Just osVar == osConfVar
212+ Arch archVar -> Just archVar == archConfVar
213+ Impl GHC _ -> True -- TODO: Actually check version range
214+ _ -> False
154215 case condLibrary <$> result of
155216 Right (Just condLib) -> do
156− let (_, lib) = simplifyCondTree (const $ pure True) condLib
157− pure $ CabalPackageInfo
158− { _cabalPackageInfo_packageRoot = dir
217+ let (_, lib) = simplifyCondTree evalConfVar condLib
218+ pure $ (warnings,) $ CabalPackageInfo
219+ { _cabalPackageInfo_packageName = T.pack packageName
220+ , _cabalPackageInfo_packageFile = packageFile
221+ , _cabalPackageInfo_packageRoot = takeDirectory packageFile
159222 , _cabalPackageInfo_sourceDirs =
160223 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
161224 , _cabalPackageInfo_defaultExtensions =
@@ -168,45 +231,49 @@ parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< r
168231 Left (_, errors) ->
169232 throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
170233
171−-- | Create ghci configuration to load the given packages
172−withGhciScript
173− :: MonadObelisk m
174− => [FilePath] -- ^ List of packages to load into ghci
175− -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
176− -> m ()
177−withGhciScript dirs f = do
178− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM dirs $ \dir -> do
234+parsePackagesOrFail :: MonadObelisk m => [FilePath] -> m (NE.NonEmpty CabalPackageInfo)
235+parsePackagesOrFail dirs = do
236+ (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
179237 flip fmap (parseCabalPackage dir) $ \case
180238 Nothing -> Left dir
181239 Just packageInfo -> Right packageInfo
182240
183− when (null packageInfos) $
184− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " dirs
241+ packageInfos <- case NE.nonEmpty packageInfos' of
242+ Nothing -> failWith $ T.pack $ "No valid packages found in " <> intercalate ", " dirs
243+ Just xs -> pure xs
244+
185245 unless (null pkgDirErrs) $
186− putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
246+ putLog Warning $ T.pack $ "Failed to find packages in " <> intercalate ", " pkgDirErrs
187247
188− let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
189− languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
190− -- NOTE when no default-language is present cabal sets Haskell98
191− language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
192− extensionsLine = if extensions == mempty
193− then ""
194− else ":set " <> unwords (("-X" <>) . prettyShow <$> extensions)
195− ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
196− packageInfos >>= _cabalPackageInfo_compilerOptions
197− dotGhci = unlines
198− [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
199− , case ghcOptions of
200− [] -> ""
201− xs -> ":set " <> unwords xs
202− , extensionsLine
203− , ":set " <> unwords (("-X" <>) . prettyShow <$> language)
204− , ":load Backend Frontend"
205− , "import Obelisk.Run"
206− , "import qualified Frontend"
207− , "import qualified Backend"
208− ]
209− warnDifferentLanguages language
248+ pure packageInfos
249+
250+packageInfoToNamePathMap :: [CabalPackageInfo] -> Map Text FilePath
251+packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot)
252+
253+-- | Create ghci configuration to load the given packages
254+withGhciScript
255+ :: MonadObelisk m
256+ => [CabalPackageInfo] -- ^ List of packages to load into ghci
257+ -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
258+ -> m ()
259+withGhciScript packageInfos f = do
260+ selfExe <- liftIO getExecutablePath
261+ let
262+ packageNames = Set.fromList $ map _cabalPackageInfo_packageName packageInfos
263+ modulesToLoad = mconcat
264+ [ [ "Obelisk.Run" | "obelisk-run" `Set.member` packageNames ]
265+ , [ "Backend" | "backend" `Set.member` packageNames ]
266+ , [ "Frontend" | "frontend" `Set.member` packageNames ]
267+ ]
268+ dotGhci = unlines
269+ -- TODO: Shell escape
270+ [ ":set -pgmF " <> selfExe <> " -optF " <> preprocessorIdentifier <> " " <> unwords (map (("-optF " <>) . _cabalPackageInfo_packageRoot) packageInfos)
271+ , ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
272+ , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
273+ , "import qualified Obelisk.Run"
274+ , "import qualified Frontend"
275+ , "import qualified Backend"
276+ ]
210277 withSystemTempDirectory "ob-ghci" $ \fp -> do
211278 let dotGhciPath = fp </> ".ghci"
212279 liftIO $ writeFile dotGhciPath dotGhci
@@ -216,24 +283,26 @@ withGhciScript dirs f = do
216283 rootedSourceDirs pkg = NE.toList $
217284 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
218285
219−warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
220−warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
221−warnDifferentLanguages _ = return ()
222−
223286-- | Run ghci repl
224287runGhciRepl
225288 :: MonadObelisk m
226− => FilePath -- ^ Path to .ghci
289+ => [CabalPackageInfo]
290+ -> FilePath -- ^ Path to .ghci
227291 -> m ()
228−runGhciRepl dotGhci = inProjectShell "ghc" $ "ghci " <> makeBaseGhciOptions dotGhci
292+runGhciRepl packages dotGhci = withProjectRoot "." $ \root ->
293+ -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
294+ -- ghc that is provided by the shell in the user's project.
295+ nixShellWithPkgs root True (packageInfoToNamePathMap packages) $ Just $ "ghci " <> makeBaseGhciOptions dotGhci -- TODO: Shell escape
229296
230297-- | Run ghcid
231298runGhcid
232299 :: MonadObelisk m
233300 => FilePath -- ^ Path to .ghci
301+ -> [CabalPackageInfo]
234302 -> Maybe String -- ^ Optional command to run at every reload
235303 -> m ()
236−runGhcid dotGhci mcmd = callCommand $ unwords $ $(staticWhich "ghcid") : opts
304+runGhcid dotGhci packages mcmd = withProjectRoot "." $ \root ->
305+ nixShellWithPkgs root True (packageInfoToNamePathMap packages) (Just $ unwords $ ghcidExePath : opts) -- TODO: Shell escape
237306 where
238307 opts =
239308 [ "-W"
@@ -242,7 +311,7 @@ runGhcid dotGhci mcmd = callCommand $ unwords $ $(staticWhich "ghcid") : opts
242311 , "--reload=config"
243312 , "--outputfile=ghcid-output.txt"
244313 ] <> testCmd
245− testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
314+ testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
246315
247316makeBaseGhciOptions :: FilePath -> String
248317makeBaseGhciOptions dotGhci =
lib/command/src/Obelisk/Command/Utils.hs+12−5
@@ -21,8 +21,6 @@ import Data.Maybe (maybeToList)
2121import Data.Text (Text)
2222import qualified Data.Text as T
2323import Data.Void (Void)
24−import System.Directory (canonicalizePath)
25−import System.Environment (getExecutablePath)
2624import System.Exit (ExitCode)
2725import System.Which (staticWhich)
2826import Text.Megaparsec as MP
@@ -31,12 +29,21 @@ import Text.Megaparsec.Char as MP
3129import Obelisk.App (MonadObelisk)
3230import Obelisk.CliApp
3331
34−getObeliskExe :: IO FilePath
35−getObeliskExe = getExecutablePath >>= canonicalizePath
36−
3732cp :: FilePath
3833cp = $(staticWhich "cp")
3934
35+ghcidExePath :: FilePath
36+ghcidExePath = $(staticWhich "ghcid")
37+
38+findExePath :: FilePath
39+findExePath = $(staticWhich "find")
40+
41+nixExePath :: FilePath
42+nixExePath = $(staticWhich "nix")
43+
44+nixBuildExePath :: FilePath
45+nixBuildExePath = $(staticWhich "nix-build")
46+
4047-- Check whether the working directory is clean
4148checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
4249checkGitCleanStatus repo withIgnored = do
lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+1−8
@@ -1,18 +1,11 @@
1−{-# LANGUAGE LambdaCase #-}
21{-# LANGUAGE OverloadedStrings #-}
32module Obelisk.ExecutableConfig.Lookup where
43
54import Data.Map (Map)
6−import qualified Data.Map as Map
75import Data.Text (Text)
8−import qualified Data.Text as T
96import Data.ByteString (ByteString)
10−import qualified Data.ByteString as BS
11−import System.Directory
12−import System.FilePath
13−import Control.Monad
147
15−import Obelisk.Configs.Internal.Directory
8+import Obelisk.Configs.Internal.Directory (getConfigsFromDirectory)
169
1710getConfigs :: IO (Map Text ByteString)
1811getConfigs = getConfigsFromDirectory "config"
lib/frontend/src/Obelisk/Frontend.hs+2−2
@@ -5,6 +5,7 @@
55{-# LANGUAGE LambdaCase #-}
66{-# LANGUAGE MultiParamTypeClasses #-}
77{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE PackageImports #-}
89{-# LANGUAGE RankNTypes #-}
910{-# LANGUAGE RecursiveDo #-}
1011{-# LANGUAGE ScopedTypeVariables #-}
@@ -45,7 +46,7 @@ import qualified GHCJS.DOM.History as DOM
4546import qualified GHCJS.DOM.Window as DOM
4647import Language.Javascript.JSaddle (MonadJSM, JSM, jsNull)
4748import GHCJS.DOM (currentDocument)
48−import GHCJS.DOM.Document (getHead)
49+import "ghcjs-dom" GHCJS.DOM.Document (getHead)
4950import GHCJS.DOM.Node (Node, removeChild_)
5051import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
5152import GHCJS.DOM.ParentNode (querySelectorAll)
@@ -197,7 +198,6 @@ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = d
197198 , PrimMonad m
198199 , MonadSample DomTimeline (Performable m)
199200 , DOM.MonadJSM m
200− , Monad (Performable (Client (HydrationDomBuilderT s DomTimeline m)))
201201 , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
202202 , MonadFix (Performable m)
203203 , MonadFix m
lib/frontend/src/Obelisk/Frontend/Cookie.hs+2−1
@@ -6,6 +6,7 @@
66{-# LANGUAGE FlexibleInstances #-}
77{-# LANGUAGE GeneralizedNewtypeDeriving #-}
88{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE PackageImports #-}
910{-# LANGUAGE RankNTypes #-}
1011{-# LANGUAGE TypeFamilies #-}
1112{-# LANGUAGE UndecidableInstances #-}
@@ -22,7 +23,7 @@ import Data.Text.Encoding (encodeUtf8)
2223import Reflex
2324import Reflex.Host.Class
2425import Reflex.Dom.Core
25−import GHCJS.DOM.Document (getCookie, Document)
26+import "ghcjs-dom" GHCJS.DOM.Document (getCookie, Document)
2627import GHCJS.DOM.Types (MonadJSM)
2728import Web.Cookie
2829
lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1
@@ -2,13 +2,13 @@
22module Obelisk.Snap.Extras
33 ( cachePermanently
44 , doNotCache
5+ , ensureSecure
56 , serveFileIfExists
67 , serveFileIfExistsAs
78 ) where
89
910import Control.Monad.IO.Class
1011import Data.ByteString (ByteString)
11−import Data.Monoid
1212import Data.String
1313import Snap.Core
1414import Snap.Util.FileServe