on master help sign in
Merge remote-tracking branch 'origin/master' into aa-nixpkgs1909
commit0a3e97a9ae9df4d164918aa032974dd8b729db3f authorJohn Ericson <John.Ericson@Obsidian.Systems> dateThu 2 Jan 2020, 19:11 (7 years ago) parentffd2e21070cde8
15 files changed+206−85
ChangeLog.md+16−10 FAQ.md+1−1 default.nix+12−2 dep/.gitignore+1 dep/ghcid/default.nix+7 dep/ghcid/github.json+7 lib/command/obelisk-command.cabal+1−1 lib/command/src/Obelisk/Command.hs+2−1 lib/command/src/Obelisk/Command/Deploy.hs+1−1 lib/command/src/Obelisk/Command/Project.hs+74−27 lib/command/src/Obelisk/Command/Run.hs+12−4 lib/route/src/Obelisk/Route/Frontend.hs+22−6 lib/run/obelisk-run.cabal+3 lib/run/src/Obelisk/Run.hs+28−15 lib/selftest/src/Obelisk/SelfTest.hs+19−17
ChangeLog.md+16−10
@@ -2,18 +2,24 @@
22
33This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release.
44
5−## Unreleased
6−
7−* Generalised pathSegmentEncoder, added pathFieldEncoder.
8−* Added some Prisms to the encoder library for manipulating DSums (perhaps they should get moved to dependent-sum before release?)
9−* Add "ob doc" command, which lists paths to haddock documentation for specified packages
10−* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set
11−* Add support in obelisk-route for single parameters in URL paths
12−* Bump reflex-platform so that obelisk now uses reflex-dom 0.5.2.0
5+## v0.3.0.0 - 2019-12-20
6+
7+* Generalised `pathSegmentEncoder`, added `pathFieldEncoder`.
8+* Added some `Prism`s to the encoder library for manipulating `DSum`s.
9+* Add `ob doc` command, which lists paths to haddock documentation for specified packages.
10+* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set.
11+* Add support in `obelisk-route` for single parameters in URL paths.
12+* Bump reflex-platform so that obelisk now uses reflex-dom 0.5.2.0.
1313* Use a `--pure` nix shell in `ob run` for parity with `ob repl` and more resilience against "works on my machine".
14−* Use `--keep NIX_PATH` for all pure shells so references to `<nixpkgs>` continues to work.
14+* Pin usages of `<nixpkgs>` in obelisk thunks, etc. to the nixpkgs used by the project's obelisk impl.
1515* Backport ACMEv2 support in obelisk server to regain LetsEncrypt account creation.
16−* Enable https in `ob run`
16+* Enable HTTPS in `ob run`.
17+* `ob run` now handles `ghci` errors better, and includes a custom `ghcid`
18+ version. As a result, you no longer need to have ghcid installed to
19+ use `ob run`, as we provide one for you.
20+* `ob` commands now complain less on systems with umasks other than `0022`.
21+* Ignore package environment files in `ob run` and `ob repl`.
22+* Add `Obelisk.Route.Frontend.routeLinkDynAttr`.
1723
1824## v0.2.0.0 - 2019-8-17
1925
FAQ.md+1−1
@@ -81,7 +81,7 @@ project's dependencies and,
8181Assuming we are in a project created with `ob init`, `ob run` calls (see
8282`lib/command/src/Obelisk/Command.hs`):
8383
84− nix-shell --pure --keep NIX_PATH -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
84+ nix-shell --pure -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
8585
8686where
8787
default.nix+12−2
@@ -36,6 +36,8 @@ let
3636 in {
3737 hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet dep/hnix) {}));
3838 hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
39+
40+ ghcid = self.callCabal2nix "ghcid" (hackGet ./dep/ghcid) {};
3941 })
4042
4143 pkgs.obeliskExecutableConfig.haskellOverlay
@@ -43,6 +45,7 @@ let
4345 # Add obelisk packages
4446 (self: super: let
4547 pkgs = self.callPackage ({ pkgs }: pkgs) {};
48+ onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
4649 in {
4750 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
4851
@@ -50,9 +53,16 @@ let
5053 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (cleanSource ./lib/asset/serve-snap) {};
5154 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
5255 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
53− obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) { librarySystemDepends = [ pkgs.nix ]; };
56+ obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) {
57+ librarySystemDepends = [
58+ pkgs.nix
59+ (haskellLib.justStaticExecutables self.ghcid)
60+ ];
61+ };
5462 obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
55− obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
63+ obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {}) (pkg:
64+ haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
65+ );
5666 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
5767 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
5868 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
dep/.gitignore+1
@@ -1,4 +1,5 @@
11*
2+!*/
23!.gitignore
34!*/*.json
45!*/default.nix
dep/ghcid/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/ghcid/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "ndmitchell",
3+ "repo": "ghcid",
4+ "branch": "master",
5+ "rev": "f572318f32b1617f6054248e5888af68222f8e50",
6+ "sha256": "1icg3r70lg2kmd9gdc024ih1n9nrja98yav74z9nvykqygvv5w0n"
7+}
lib/command/obelisk-command.cabal+1−1
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.1
2+version: 0.3
33cabal-version: >= 1.8
44build-type: Simple
55
lib/command/src/Obelisk/Command.hs+2−1
@@ -267,7 +267,8 @@ mkObeliskConfig :: IO Obelisk
267267mkObeliskConfig = do
268268 cliArgs <- getArgs
269269 -- This function should not use argument parser (full argument parsing happens post handoff)
270− let logLevel = toLogLevel $ "-v" `elem` cliArgs
270+ -- TODO: See if we can use the argument parser with a subset of the parsers to get logging level out.
271+ let logLevel = toLogLevel $ any (`elem` ["-v", "--verbose"]) cliArgs
271272 notInteractive <- not <$> isInteractiveTerm
272273 cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
273274 ObeliskError_ProcessError (ProcessFailure p code) ann ->
lib/command/src/Obelisk/Command/Deploy.hs+1−1
@@ -281,7 +281,7 @@ createKeystore root config = do
281281 [ "-genkeypair", "-noprompt"
282282 , "-keystore", _keytoolConfig_keystore config
283283 , "-keyalg", "RSA", "-keysize", "2048"
284− , "-validity", "1000000000"
284+ , "-validity", "1000000"
285285 , "-storepass", _keytoolConfig_storepass config
286286 , "-alias", _keytoolConfig_alias config
287287 , "-keypass", _keytoolConfig_keypass config
lib/command/src/Obelisk/Command/Project.hs+74−27
@@ -1,6 +1,7 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE LambdaCase #-}
33{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE TemplateHaskell #-}
45module Obelisk.Command.Project
56 ( InitSource (..)
67 , initProject
@@ -15,6 +16,7 @@ module Obelisk.Command.Project
1516 , toImplDir
1617 ) where
1718
19+import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
1820import Control.Monad
1921import Control.Monad.IO.Class (liftIO)
2022import Control.Monad.State
@@ -22,11 +24,14 @@ import Data.Bits
2224import Data.Function (on)
2325import qualified Data.Text as T
2426import System.Directory
27+import System.Environment (lookupEnv)
2528import System.FilePath
2629import System.IO.Temp
27−import System.Posix (FileStatus, UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
30+import System.IO.Unsafe (unsafePerformIO)
31+import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
2832import System.Posix.Files
2933import System.Process (CreateProcess, cwd, proc, waitForProcess, delegate_ctlc)
34+import System.Which (staticWhich)
3035
3136import GitHub.Data.GitData (Branch)
3237import GitHub.Data.Name (Name)
@@ -34,6 +39,7 @@ import GitHub.Data.Name (Name)
3439import Obelisk.App (MonadObelisk)
3540import Obelisk.CliApp
3641import Obelisk.Command.Thunk
42+
3743--TODO: Make this module resilient to random exceptions
3844
3945--TODO: Don't hardcode this
@@ -50,10 +56,10 @@ obeliskSourceWithBranch branch = ThunkSource_GitHub $ GitHubSource
5056 }
5157
5258data InitSource
53− = InitSource_Default
54− | InitSource_Branch (Name Branch)
55− | InitSource_Symlink FilePath
56− deriving Show
59+ = InitSource_Default
60+ | InitSource_Branch (Name Branch)
61+ | InitSource_Symlink FilePath
62+ deriving Show
5763
5864-- | Path to obelisk directory in given path
5965toObeliskDir :: FilePath -> FilePath
@@ -114,15 +120,16 @@ initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
114120findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
115121findProjectObeliskCommand target = do
116122 myUid <- liftIO getRealUserID
123+ processUmask <- liftIO getUmask
117124 targetStat <- liftIO $ getFileStatus target
118− (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
125+ (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat processUmask myUid >>= \case
119126 Nothing -> pure Nothing
120127 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
121128 False -> do
122129 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
123130 pure Nothing
124131 True -> do
125− walkToImplDir projectRoot myUid -- For security check
132+ walkToImplDir projectRoot myUid processUmask -- For security check
126133 return $ Just projectRoot
127134 case (result, insecurePaths) of
128135 (Just projDir, []) -> do
@@ -133,16 +140,36 @@ findProjectObeliskCommand target = do
133140 putLog Error $ T.unlines
134141 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
135142 , T.unlines $ fmap (T.pack . normalise) insecurePaths
136− , "Please ensure that all of these directories are owned by you and are not writable by anyone else."
143+ , "Please ensure that all of these directories are owned by you, not world-writable, and no more group-writable than permitted by your umask."
137144 ]
138145 return Nothing
139146
147+-- | Get the umask for the Obelisk process.
148+--
149+-- Because of
150+-- http://man7.org/linux/man-pages/man2/umask.2.html#NOTES we have to set the
151+-- umask to read it. We are using 'withMVarMasked' to guarantee that setting and
152+-- reading isn't interrupted by any exception or interleaved with another thread.
153+getUmask :: IO FileMode
154+getUmask = withMVarMasked globalUmaskLock $ \() -> do
155+ initialMask <- setFileCreationMask safeUmask
156+ void (setFileCreationMask initialMask)
157+ pure initialMask
158+ where
159+ safeUmask :: FileMode
160+ safeUmask = CMode 0o22
161+
162+{-# NOINLINE globalUmaskLock #-}
163+globalUmaskLock :: MVar ()
164+globalUmaskLock = unsafePerformIO (newMVar ())
165+
140166-- | Get the FilePath to the containing project directory, if there is one
141167findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
142168findProjectRoot target = do
143169 myUid <- liftIO getRealUserID
144170 targetStat <- liftIO $ getFileStatus target
145− (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat myUid) []
171+ umask <- liftIO $ getUmask
172+ (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat umask myUid) []
146173 return result
147174
148175withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
@@ -155,15 +182,15 @@ withProjectRoot target f = findProjectRoot target >>= \case
155182-- traversed in the process. Return the project root directory, if found.
156183walkToProjectRoot
157184 :: (MonadState [FilePath] m, MonadIO m)
158− => FilePath -> FileStatus -> UserID -> m (Maybe FilePath)
159−walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \case
185+ => FilePath -> FileStatus -> FileMode -> UserID -> m (Maybe FilePath)
186+walkToProjectRoot this thisStat desiredUmask myUid = liftIO (doesDirectoryExist this) >>= \case
160187 -- It's not a directory, so it can't be a project
161188 False -> do
162189 let dir = takeDirectory this
163190 dirStat <- liftIO $ getFileStatus dir
164− walkToProjectRoot dir dirStat myUid
191+ walkToProjectRoot dir dirStat desiredUmask myUid
165192 True -> do
166− when (not $ isWritableOnlyBy thisStat myUid) $ modify (this:)
193+ when (not $ isWellOwnedAndWellPermissioned thisStat myUid desiredUmask) $ modify (this:)
167194 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
168195 True -> return $ Just this
169196 False -> do
@@ -173,25 +200,38 @@ walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \ca
173200 isSameFileAs = (==) `on` fileIdentity
174201 if thisStat `isSameFileAs` nextStat
175202 then return Nothing -- Found a cycle; probably hit root directory
176− else walkToProjectRoot next nextStat myUid
203+ else walkToProjectRoot next nextStat desiredUmask myUid
177204
178205-- | Walk from the given project root directory to its Obelisk implementation
179206-- directory, accumulating potentially insecure directories that were traversed
180207-- in the process.
181−walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> m ()
182−walkToImplDir projectRoot myUid = do
208+walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> FileMode -> m ()
209+walkToImplDir projectRoot myUid umask = do
183210 let obDir = toObeliskDir projectRoot
184211 obDirStat <- liftIO $ getFileStatus obDir
185− when (not $ isWritableOnlyBy obDirStat myUid) $ modify (obDir:)
212+ when (not $ isWellOwnedAndWellPermissioned obDirStat myUid umask) $ modify (obDir:)
186213 let implThunk = obDir </> "impl"
187214 implThunkStat <- liftIO $ getFileStatus implThunk
188− when (not $ isWritableOnlyBy implThunkStat myUid) $ modify (implThunk:)
215+ when (not $ isWellOwnedAndWellPermissioned implThunkStat myUid umask) $ modify (implThunk:)
189216
190−--TODO: Is there a better way to ask if anyone else can write things?
191−--E.g. what about ACLs?
192−-- | Check to see if directory is only writable by a user whose User ID matches the second argument provided
193−isWritableOnlyBy :: FileStatus -> UserID -> Bool
194−isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
217+-- | Check to see if directory is writable by a user whose User ID matches the
218+-- second argument provided, and if the fact that other people can write to that
219+-- directory is in accordance with the umask of the system, passed as the third
220+-- argument.
221+isWellOwnedAndWellPermissioned :: FileStatus -> UserID -> FileMode -> Bool
222+isWellOwnedAndWellPermissioned s uid umask = isOwnedBy s uid && filePermissionIsSafe s umask
223+
224+isOwnedBy :: FileStatus -> UserID -> Bool
225+isOwnedBy s uid = fileOwner s == uid
226+
227+-- | Check to see if a directory respect the umask, but check explicitly that
228+-- it's not world writable in any case.
229+filePermissionIsSafe :: FileStatus -> FileMode -> Bool
230+filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= umaskGroupWritable
231+ where
232+ fileWorldWritable = fileMode s .&. 0o002 == 0o002
233+ fileGroupWritable = fileMode s .&. 0o020 == 0o020
234+ umaskGroupWritable = umask .&. 0o020 == 0
195235
196236-- | Run a command in the given shell for the current project
197237inProjectShell :: MonadObelisk m => String -> String -> m ()
@@ -204,16 +244,23 @@ inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
204244
205245projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
206246projectShell root isPure shellName command = do
247+ let nixPath = $(staticWhich "nix")
248+ nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ proc nixPath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
249+ nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
207250 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
208251 [ "default.nix"] <>
209− -- Keep $NIX_PATH in the env for --pure shells so '<nixpkgs>' works in sub-commands
210− -- TODO: Don't use <nixpkgs> for anything!
211− (if isPure then [ "--pure", "--keep", "NIX_PATH" ] else []) <>
252+ ["--pure" | isPure] <>
212253 [ "-A"
213254 , "shells." <> shellName
214255 ] <> case command of
215256 Nothing -> []
216− Just c -> ["--run", c]
257+ Just c ->
258+ -- TODO: Escape nixpkgsPath and nixRemote
259+ [ "--run"
260+ , "export NIX_PATH=nixpkgs=" <> T.unpack nixpkgsPath <> "; " <>
261+ maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote <>
262+ c
263+ ]
217264 void $ liftIO $ waitForProcess ph
218265
219266setCtlc :: CreateProcess -> CreateProcess
lib/command/src/Obelisk/Command/Run.hs+12−4
@@ -219,7 +219,7 @@ runGhciRepl
219219 :: MonadObelisk m
220220 => FilePath -- ^ Path to .ghci
221221 -> m ()
222−runGhciRepl dotGhci = inProjectShell "ghc" $ unwords $ "ghci" : ["-no-user-package-db", "-ghci-script", dotGhci]
222+runGhciRepl dotGhci = inProjectShell "ghc" $ "ghci " <> makeBaseGhciOptions dotGhci
223223
224224-- | Run ghcid
225225runGhcid
@@ -227,17 +227,25 @@ runGhcid
227227 => FilePath -- ^ Path to .ghci
228228 -> Maybe String -- ^ Optional command to run at every reload
229229 -> m ()
230−runGhcid dotGhci mcmd = callCommand $ unwords $ "ghcid" : opts
230+runGhcid dotGhci mcmd = callCommand $ unwords $ $(staticWhich "ghcid") : opts
231231 where
232232 opts =
233233 [ "-W"
234234 --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
235− , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -no-user-package-db -ghci-script " <> dotGhci <> "' "
235+ , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints " <> makeBaseGhciOptions dotGhci <> "' "
236236 , "--reload=config"
237237 , "--outputfile=ghcid-output.txt"
238238 ] <> testCmd
239239 testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
240240
241+makeBaseGhciOptions :: FilePath -> String
242+makeBaseGhciOptions dotGhci =
243+ unwords
244+ [ "-no-user-package-db"
245+ , "-package-env -"
246+ , "-ghci-script " <> dotGhci
247+ ]
248+
241249getFreePort :: MonadIO m => m PortNumber
242250getFreePort = liftIO $ withSocketsDo $ do
243251 addr:_ <- getAddrInfo (Just defaultHints) (Just "127.0.0.1") (Just "0")
@@ -246,4 +254,4 @@ getFreePort = liftIO $ withSocketsDo $ do
246254 open addr = do
247255 sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
248256 bind sock (addrAddress addr)
249− return sock
257+ return sock
lib/route/src/Obelisk/Route/Frontend.hs+22−6
@@ -53,6 +53,7 @@ module Obelisk.Route.Frontend
5353 , runRouteToUrlT
5454 , mapRouteToUrlT
5555 , routeLink
56+ , routeLinkDynAttr
5657 , dynRouteLink
5758 , adaptedUriPath
5859 , setAdaptedUriPath
@@ -74,6 +75,7 @@ import Control.Monad.Trans.Control
7475import Data.Coerce
7576import Data.Dependent.Sum (DSum (..))
7677import Data.GADT.Compare
78+import Data.Map (Map)
7779import Data.Monoid
7880import Data.Proxy
7981import Data.Text (Text)
@@ -526,9 +528,7 @@ routeLink r w = do
526528 setRoute $ r <$ domEvent Click e
527529 return a
528530
529−-- | A link widget that, when clicked, sets the route to current value of the
530−-- provided dynamic route. In non-javascript contexts the value of the dynamic post
531−-- build is used so the link still works like 'routeLink'.
531+-- | Like 'routeLinkDynAttr' but without custom attributes.
532532dynRouteLink
533533 :: forall t m a route.
534534 ( DomBuilder t m
@@ -537,11 +537,27 @@ dynRouteLink
537537 , SetRoute t (R route) m
538538 )
539539 => Dynamic t (R route) -- ^ Target route
540− -> m a -- ^ Child widget
540+ -> m a -- ^ Child widget of the @a@ element
541+ -> m a
542+dynRouteLink = routeLinkDynAttr mempty
543+
544+-- | An @a@-tag link widget that, when clicked, sets the route to current value of the
545+-- provided dynamic route. In non-JavaScript contexts the value of the dynamic post
546+-- build is used so the link still works like 'routeLink'.
547+routeLinkDynAttr
548+ :: forall t m a route.
549+ ( DomBuilder t m
550+ , PostBuild t m
551+ , RouteToUrl (R route) m
552+ , SetRoute t (R route) m
553+ )
554+ => Dynamic t (Map AttributeName Text) -- ^ Attributes for @a@ element. Note that if @href@ is present it will be ignored
555+ -> Dynamic t (R route) -- ^ Target route
556+ -> m a -- ^ Child widget of the @a@ element
541557 -> m a
542−dynRouteLink dr w = do
558+routeLinkDynAttr dAttr dr w = do
543559 enc <- askRouteToUrl
544− er <- dynamicAttributesToModifyAttributes $ ("href" =:) . enc <$> dr
560+ er <- dynamicAttributesToModifyAttributes $ zipDynWith (<>) (("href" =:) . enc <$> dr) dAttr
545561 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
546562 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
547563 & elementConfig_modifyAttributes .~ er
lib/run/obelisk-run.cabal+3
@@ -48,3 +48,6 @@ library
4848 exposed-modules:
4949 Obelisk.Run
5050 ghc-options: -Wall -fwarn-redundant-constraints
51+ if os(linux)
52+ cpp-options: -DIPROUTE_SUPPORTED
53+ build-depends: which
lib/run/src/Obelisk/Run.hs+28−15
@@ -1,3 +1,8 @@
1+{-# LANGUAGE CPP #-}
2+#if defined(IPROUTE_SUPPORTED)
3+{-# LANGUAGE TemplateHaskell #-}
4+#endif
5+
16{-# LANGUAGE ConstraintKinds #-}
27{-# LANGUAGE FlexibleContexts #-}
38{-# LANGUAGE FlexibleInstances #-}
@@ -62,12 +67,15 @@ import Snap.Core (Snap)
6267import System.Environment
6368import System.IO
6469import System.Process
65−import System.Exit (ExitCode(..))
6670import Text.URI (URI)
6771import qualified Text.URI as URI
6872import Text.URI.Lens
6973import Web.Cookie
7074
75+#if defined(IPROUTE_SUPPORTED)
76+import qualified System.Which
77+#endif
78+
7179run
7280 :: Int -- ^ Port to run the backend
7381 -> ([Text] -> Snap ()) -- ^ Static asset handler
@@ -99,7 +107,7 @@ runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate
99107
100108getConfigRoute :: Map Text ByteString -> Either Text URI
101109getConfigRoute configs = case Map.lookup "common/route" configs of
102− Just r ->
110+ Just r ->
103111 let stripped = T.strip (T.decodeUtf8 r)
104112 in case URI.mkURI stripped of
105113 Just route -> Right route
@@ -122,20 +130,20 @@ runWidget conf configs frontend validFullEncoder = do
122130 settings = setBeforeMainLoop beforeMainLoop (setPort port (setTimeout 3600 defaultSettings))
123131 -- Providing TLS here will also incidentally provide it to proxied requests to the backend.
124132 prepareRunner = case (uri ^? uriScheme . _Just . unRText) of
125− Just "https" -> do
133+ Just "https" -> do
126134 -- Generate a private key and self-signed certificate for TLS
127135 privateKey <- RSA.generateRSAKey' 2048 3
128136
129137 certRequest <- X509Request.newX509Req
130138 _ <- X509Request.setPublicKey certRequest privateKey
131− _ <- X509Request.signX509Req certRequest privateKey Nothing
139+ _ <- X509Request.signX509Req certRequest privateKey Nothing
132140
133141 cert <- X509.newX509 >>= X509Request.makeX509FromReq certRequest
134142 _ <- X509.setPublicKey cert privateKey
135143 now <- getCurrentTime
136144 _ <- X509.setNotBefore cert $ addUTCTime (-1) now
137145 _ <- X509.setNotAfter cert $ addUTCTime (365 * 24 * 60 * 60) now
138− _ <- X509.signX509 cert privateKey Nothing
146+ _ <- X509.signX509 cert privateKey Nothing
139147
140148 certByteString <- BSUTF8.fromString <$> PEM.writeX509 cert
141149 privateKeyByteString <- BSUTF8.fromString <$> PEM.writePKCS8PrivateKey privateKey Nothing
@@ -212,17 +220,22 @@ logPortBindErr p e = getProcessIdForPort p >>= \case
212220 Nothing -> putStrLn $ "runWidget: " <> show e
213221 Just pid -> putStrLn $ unwords [ "Port", show p, "is being used by process ID", show pid <> ".", "Please kill that process or change the port in config/common/route."]
214222
223+ssPath :: Maybe String
224+ssPath =
225+#if defined(IPROUTE_SUPPORTED)
226+ Just $(System.Which.staticWhich "ss")
227+#else
228+ Nothing
229+#endif
230+
215231getProcessIdForPort :: Int -> IO (Maybe Int)
216−getProcessIdForPort port = do
217− -- First check if 'ss' is available
218− (c, _, _) <- readProcessWithExitCode "which" ["ss"] mempty
219− case c of
220− ExitSuccess -> do
221− xs <- lines <$> readProcess "ss" ["-lptn", "sport = " <> show port] mempty
222− case uncons xs of
223− Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
224− _ -> return Nothing
225− _ -> return Nothing
232+getProcessIdForPort port = case ssPath of
233+ Just ss -> do
234+ xs <- lines <$> readProcess ss ["-lptn", "sport = " <> show port] mempty
235+ case uncons xs of
236+ Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
237+ _ -> return Nothing
238+ _ -> return Nothing
226239
227240parseSsPid :: A.Parser Int
228241parseSsPid = do
lib/selftest/src/Obelisk/SelfTest.hs+19−17
@@ -3,7 +3,6 @@
33{-# LANGUAGE OverloadedStrings #-}
44{-# LANGUAGE PatternGuards #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TypeApplications #-}
76module Obelisk.SelfTest where
87
98import Control.Exception (bracket, throw)
@@ -71,7 +70,7 @@ shellyOb f obTest = shelly $ f obTest
7170main :: IO ()
7271main = do
7372 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
74− isVerbose <- (elem "-v") <$> getArgs
73+ isVerbose <- elem "-v" <$> getArgs
7574 unless isVerbose $
7675 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
7776 let verbosity = bool silently verbosely isVerbose
@@ -118,7 +117,7 @@ main = do
118117 it "works with master branch impl" $ inTmp $ \_ -> run "ob" ["init", "--branch", "master"]
119118 it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", obeliskImpl]
120119 it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init" $ \dir -> do
121− let p force = (proc "ob" $ "--no-handoff" : "init" : if force then ["--force"] else []) { cwd = Just dir }
120+ let p force = (proc "ob" $ "--no-handoff" : "init" : ["--force"|force]) { cwd = Just dir }
122121 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
123122 (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
124123 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
@@ -128,7 +127,7 @@ main = do
128127 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
129128 ls tmp >>= liftIO . assertEqual "" []
130129
131− it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
130+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
132131 withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
133132 configs <- getConfigs
134133 return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
@@ -136,10 +135,8 @@ main = do
136135 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
137136 -- when not run by 'nix-build --attr selftest'
138137 describe "ob run" $ parallel $ do
139− it "works in root directory" $ inTmpObInit $ \_ -> do
140− testObRunInDir p0 p1 Nothing httpManager
141− it "works in sub directory" $ inTmpObInit $ \_ -> do
142− testObRunInDir p2 p3 (Just "frontend") httpManager
138+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir p0 p1 Nothing httpManager
139+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir p2 p3 (Just "frontend") httpManager
143140
144141 describe "obelisk project" $ parallel $ do
145142 it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , obeliskImpl]
@@ -149,14 +146,19 @@ main = do
149146
150147 describe "blank initialized project" $ parallel $ do
151148
152− it "can build ghc.backend" $ inTmpObInit $ \_ -> do
153− nixBuild ["-A", "ghc.backend"]
154− it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
155− nixBuild ["-A", "ghcjs.frontend"]
149+ it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
150+ it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
156151
157152 if os == "darwin"
158− then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
159− else it "can build android" $ inTmpObInit $ \_ -> nixBuild ["-A", "android.frontend"]
153+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
154+ else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
155+ let defaultNixPath = dir </> ("default.nix" :: Shelly.FilePath)
156+ writefile defaultNixPath
157+ =<< T.replace
158+ "# config.android_sdk.accept_license = false;"
159+ "config.android_sdk.accept_license = true;"
160+ <$> readfile defaultNixPath
161+ nixBuild ["-A", "android.frontend"]
160162
161163 forM_ ["ghc", "ghcjs"] $ \compiler -> do
162164 let
@@ -191,7 +193,7 @@ main = do
191193 branch' <- chdir dir $ run "git" ["rev-parse", "--abbrev-ref", "HEAD"]
192194 liftIO $ assertEqual "" branch (T.strip branch')
193195
194− it "can pack and unpack plain git repos" $ do
196+ it "can pack and unpack plain git repos" $
195197 shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
196198 let repo = toTextIgnore $ dir </> ("repo" :: String)
197199 run_ "git" ["clone", "https://github.com/haskell/process.git", repo]
@@ -210,7 +212,7 @@ main = do
210212 testThunkPack $ fromText repo
211213
212214 it "aborts thunk pack when there are uncommitted files" $ inTmpObInit $ \dir -> do
213− void $ unpack
215+ void unpack
214216 testThunkPack (dir </> thunk)
215217
216218 describe "ob thunk update --branch" $ parallel $ do
@@ -289,7 +291,7 @@ alterRouteTo uri stdout = do
289291
290292-- | Handle stdout of `ob run`: check that the frontend and backend servers are started correctly
291293handleObRunStdout :: HTTP.Manager -> Handle -> Sh Text
292−handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
294+handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) ->
293295 liftIO (T.hGetLine stdout) >>= \t -> case state of
294296 ObRunState_Init
295297 | "Running test..." `T.isPrefixOf` t -> loop (ObRunState_BackendStarted, msgs)