on master help sign in
Merge branch 'is-apk-signing-revision' of github.com:obsidiansystems/obelisk into is-apk-signing-revision
commitdc992e307927735e30726e4e21170cc4310a0266 authorhsloan <ishaqsloan@gmail.com> dateThu 15 Nov 2018, 20:46 (8 years ago) parent6719b928d665d0
20 files changed+560−389
README.md+21−26 default.nix+23−13 dep/reflex-platform/github.json+2−2 lib/asset/manifest/src-bin/generate.hs−1 lib/asset/manifest/src/Obelisk/Asset/Promoted.hs+1−9 lib/backend/obelisk-backend.cabal+1−1 lib/backend/src/Obelisk/Backend.hs+35−16 lib/command/src/Obelisk/Command/Deploy.hs+138−83 lib/command/src/Obelisk/Command/Nix.hs+132−40 lib/command/src/Obelisk/Command/Project.hs+1−1 lib/command/src/Obelisk/Command/Run.hs+20−5 lib/command/src/Obelisk/Command/Thunk.hs+26−16 lib/frontend/src/Obelisk/Frontend.hs+11−8 lib/route/src/Obelisk/Route.hs+2 lib/route/src/Obelisk/Route/Frontend.hs+108−2 lib/run/src/Obelisk/Run.hs+8−6 lib/selftest/src/Obelisk/SelfTest.hs+31−20 lib/snap/obelisk-snap.cabal−40 lib/snap/src/Obelisk/Snap.hs−99 release.nix−1
README.md+21−26
@@ -47,39 +47,23 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
4747When developing on obelisk itself you may launch `ghcid` for the corresponding project as follows. For example to launch ghcid for `lib/backend` project:
4848
4949```
50−nix-shell -A obelisk.obelisk-backend.env --run "cd lib/backend && ghcid -c 'cabal new-repl'"
50+nix-shell -A obeliskEnvs.obelisk-backend --run "cd lib/backend && ghcid -c 'cabal new-repl'"
5151```
5252
5353Or to launch ghcid for `lib/command` project:
5454
5555```
56−nix-shell -A obelisk.obelisk-command.env --run "cd lib/command && ghcid -c 'cabal new-repl'"
56+nix-shell -A obeliskEnvs.obelisk-command --run "cd lib/command && ghcid -c 'cabal new-repl'"
5757```
5858
5959### Accessing private repositories
60−To allow the Nix builder to access private git repositories, follow these steps:
61−
62−1. [Get set up to connect to GitHub with SSH](https://help.github.com/articles/connecting-to-github-with-ssh/)
63−1. [Create a GitHub personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)
64−1. Set environment variables:
65− * NixOS: Add this to `/etc/nixos/configuration.nix`:
66− ```
67− nix.envVars = {
68− NIX_GITHUB_PRIVATE_USERNAME = "your-github-username";
69− NIX_GITHUB_PRIVATE_PASSWORD = "your-github-personal-access-token";
70− };
71− ```
72− * MacOS:
73− ```
74− sudo launchctl setenv NIX_GITHUB_PRIVATE_USERNAME "your-github-username"
75− sudo launchctl setenv NIX_GITHUB_PRIVATE_PASSWORD "your-github-personal-access"
76− sudo launchctl stop org.nixos.nix-daemon
77− sudo launchctl start org.nixos.nix-daemon
78− ```
79−1. `nix-env -i hub` OR `nix-env -iA nixos.gitAndTools.hub`
80−1. `hub clone yourusername/yourproject`
81− * NOTE: you must authenticate with hub at least once, because the `ob` command uses `hub` for authentication
82− #TODO: Make ob do this itself (either invoke hub automatically or not depend on hub)
60+
61+To allow the Nix builder to access private git repositories, you must be set up
62+to access them via SSH. Follow these steps depending on the platform you need
63+access to:
64+
65+- [GitHub](https://help.github.com/articles/connecting-to-github-with-ssh/)
66+- [GitLab](https://docs.gitlab.com/ee/gitlab-basics/create-your-ssh-keys.html)
8367
8468## Developing an Obelisk project
8569
@@ -156,7 +140,7 @@ ob deploy push
156140
157141`ob deploy push` will locally build your app and then transfer it, along with all the Nix package dependencies, via ssh to the EC2 instance. The backend will live in `/var/lib/backend`.
158142
159−At this point you are done. Your app will be accessible at `${ROUTE}`.
143+At this point you are done. Your app will be accessible at `${ROUTE}`. The currently deployed version - the git commit hash of the source repo - can be found at `${ROUTE}/version`.
160144
161145### From macOS
162146
@@ -263,6 +247,17 @@ Now deploy the built apk file to your Android device:
2632471. Connect the device using USB (be sure to confirm any security prompts on the device)
2642481. Run the deploy script: `result-android/bin/deploy`
265249
250+Alternatively, you can deploy from an obelisk deployment directory (a directory generated post `ob deploy init ...` command) using the `ob deploy test android` command.
251+This command will accomplish the following:
252+
253+1. Create a key store and apk signing key (`android_keystore.jks`)
254+1. Build a Signed Android apk for your application
255+1. Deploy the Signed apk to your connected Android device
256+
257+In the event that you change your key or keystore password, you will have to update your credentials within the JSON object found in `android_keytool_config.json`
258+
259+Additional documentation on java key stores can be found [here] (https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html)
260+
266261This should copy over and install the application on your device (if you see a "*signatures do not match*" error, simply uninstall the previous app from the device before retrying the deploy). The name of the installed application will be what you have specified for `android.displayName` in the `default.nix`.
267262
268263#### Releasing to Play Store
default.nix+23−13
@@ -61,7 +61,6 @@ let
6161 obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
6262 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
6363 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
64− obelisk-snap = self.callCabal2nix "obelisk-snap" (cleanSource ./lib/snap) {};
6564 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
6665 })
6766
@@ -115,6 +114,9 @@ let
115114 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
116115 ghcObelisk = reflex-platform.ghc;
117116
117+ # Development environments for obelisk packages.
118+ ghcObeliskEnvs = pkgs.lib.mapAttrs (n: v: reflex-platform.workOn ghcObelisk v) ghcObelisk;
119+
118120 inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
119121
120122 haskellLib = pkgs.haskell.lib;
@@ -126,6 +128,7 @@ in rec {
126128 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
127129 path = reflex-platform.filterGit ./.;
128130 obelisk = ghcObelisk;
131+ obeliskEnvs = ghcObeliskEnvs;
129132 command = ghcObelisk.obelisk-command;
130133 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
131134
@@ -135,7 +138,7 @@ in rec {
135138
136139 PATH="${command}/bin:$PATH"
137140 export OBELISK_IMPL="${hackGet ./.}"
138− "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" "$@"
141+ "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
139142 '';
140143 #TODO: Why can't I build ./skeleton directly as a derivation? `nix-build -E ./.` doesn't work
141144 skeleton = pkgs.runCommand "skeleton" {
@@ -148,9 +151,7 @@ in rec {
148151 processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
149152 inherit src;
150153 outputs = [ "out" "haskellManifest" "symlinked" ];
151− buildInputs = [
152− (reflex-platform.ghc.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {})
153− ];
154+ nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
154155 } ''
155156 set -euo pipefail
156157 touch "$out"
@@ -233,7 +234,7 @@ in rec {
233234 };
234235 };
235236
236− serverExe = backend: frontend: assets: config: optimizationLevel:
237+ serverExe = backend: frontend: assets: config: optimizationLevel: version:
237238 pkgs.runCommand "serverExe" {} ''
238239 mkdir $out
239240 set -eux
@@ -241,9 +242,10 @@ in rec {
241242 ln -s "${mkAssets assets}" $out/static.assets
242243 cp -r ${config} $out/config
243244 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
245+ echo ${version} > $out/version
244246 '';
245247
246− server = { exe, hostName, adminEmail, routeHost, enableHttps, config }@args:
248+ server = { exe, hostName, adminEmail, routeHost, enableHttps, config, version }@args:
247249 let
248250 nixos = import (pkgs.path + /nixos);
249251 in nixos {
@@ -275,6 +277,7 @@ in rec {
275277 backendName = "backend";
276278 commonName = "common";
277279 staticName = "obelisk-generated-static";
280+ staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
278281 processedStatic = processAssets { src = staticFiles; };
279282 # The packages whose names and roles are defined by this package
280283 predefinedPackages = lib.filterAttrs (_: x: x != null) {
@@ -326,18 +329,25 @@ in rec {
326329 nixpkgs.obeliskExecutableConfig.platforms.ios.inject injectableConfig processedStatic.symlinked;
327330 } // ios;
328331 };
329− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles __closureCompilerOptimizationLevel; };
332+ passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel; };
330333 };
331334 in mkProject (projectDefinition args));
332− serverOn = sys: config: serverExe (projectOut sys).ghc.backend (projectOut system).ghcjs.frontend (projectOut sys).passthru.staticFiles config (projectOut sys).passthru.__closureCompilerOptimizationLevel;
335+ serverOn = sys: config: version: serverExe
336+ (projectOut sys).ghc.backend
337+ (projectOut system).ghcjs.frontend
338+ (projectOut sys).passthru.staticFiles
339+ config
340+ (projectOut sys).passthru.__closureCompilerOptimizationLevel
341+ version;
333342 linuxExe = serverOn "x86_64-linux";
343+ dummyVersion = "Version number is only available for deployments";
334344 in projectOut system // {
335345 linuxExeConfigurable = linuxExe;
336− linuxExe = linuxExe (base + "/config");
337− exe = serverOn system (base + "/config") ;
338− server = args@{ hostName, adminEmail, routeHost, enableHttps, config }: let
346+ linuxExe = linuxExe (base + "/config") dummyVersion;
347+ exe = serverOn system (base + "/config") dummyVersion;
348+ server = args@{ hostName, adminEmail, routeHost, enableHttps, config, version }: let
339349 injectableConfig = builtins.filterSource (path: _: !(lib.hasPrefix (toString config + "/backend") (toString path))) config;
340− in server (args // { exe = linuxExe injectableConfig; });
350+ in server (args // { exe = linuxExe injectableConfig version; });
341351 obelisk = import (base + "/.obelisk/impl") {};
342352 };
343353 haskellPackageSets = {
dep/reflex-platform/github.json+2−2
@@ -2,6 +2,6 @@
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
44 "branch": "develop",
5− "rev": "018576456043fe8372a0332c93245a4a620c1fb7",
6− "sha256": "0qhnyyf96iajz97pbqfsw2l1kkbr1yq3bvw3qhjk031vdrkv7b7y"
5+ "rev": "a721a268120908c9902a8b20509e6983b7088161",
6+ "sha256": "1w3ka16qsxkilvhvn7vdywhnhaybv6z587rmjbbqz0qmd9rlk8yc"
77}
lib/asset/manifest/src-bin/generate.hs−1
@@ -12,6 +12,5 @@ main = do
1212 writeStaticProject paths haskellTarget $ StaticConfig
1313 { _staticConfig_packageName = T.pack packageName
1414 , _staticConfig_moduleName = T.pack moduleName
15− , _staticConfig_rootPath = root
1615 }
1716 copyAndSymlink paths root fileTarget
lib/asset/manifest/src/Obelisk/Asset/Promoted.hs+1−9
@@ -28,7 +28,6 @@ import qualified Data.Map as Map
2828data StaticConfig = StaticConfig
2929 { _staticConfig_packageName :: Text --TODO: Better type
3030 , _staticConfig_moduleName :: Text --TODO: Better type
31− , _staticConfig_rootPath :: FilePath
3231 }
3332
3433writeStaticProject :: Map FilePath FilePath -> FilePath -> StaticConfig -> IO ()
@@ -41,14 +40,7 @@ writeStaticProject paths target cfg = do
4140 Just (name, parents) -> (name, target </> "src" </> T.unpack (T.intercalate "/" $ reverse parents))
4241 createDirectoryIfMissing True moduleDirPath
4342 modContents <- staticModuleFile modName paths
44− T.writeFile (moduleDirPath </> T.unpack modName' <.> "hs") $ appendRootPath (_staticConfig_rootPath cfg) modContents
45−
46−appendRootPath :: FilePath -> Text -> Text
47−appendRootPath fp txt = T.unlines
48− [ txt
49− , "staticRootPath :: FilePath"
50− , "staticRootPath = \"" <> T.pack fp <> "\""
51− ]
43+ T.writeFile (moduleDirPath </> T.unpack modName' <.> "hs") modContents
5244
5345staticCabalFile :: StaticConfig -> Text
5446staticCabalFile cfg = T.unlines
lib/backend/obelisk-backend.cabal+1−1
@@ -17,7 +17,7 @@ library
1717 obelisk-executable-config-inject,
1818 obelisk-frontend,
1919 obelisk-route,
20− obelisk-snap,
20+ obelisk-snap-extras,
2121 reflex-dom,
2222 snap,
2323 snap-server,
lib/backend/src/Obelisk/Backend.hs+35−16
@@ -5,6 +5,7 @@
55{-# LANGUAGE LambdaCase #-}
66{-# LANGUAGE MultiParamTypeClasses #-}
77{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
89{-# LANGUAGE TemplateHaskell #-}
910module Obelisk.Backend
1011 ( Backend (..)
@@ -16,6 +17,8 @@ module Obelisk.Backend
1617 , serveDefaultObeliskApp
1718 , prettifyOutput
1819 , runBackend
20+ , staticRenderContentType
21+ , mkRouteToUrl
1922 ) where
2023
2124import Prelude hiding (id, (.))
@@ -37,9 +40,10 @@ import Obelisk.Asset.Serve.Snap (serveAsset)
3740import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
3841import Obelisk.Frontend
3942import Obelisk.Route
43+import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
4044import Reflex.Dom
41−import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, rqPathInfo,
42− rqQueryString, writeBS, writeText)
45+import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
46+ , rqPathInfo, rqQueryString, setContentType, writeBS, writeText)
4347import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
4448import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
4549
@@ -56,8 +60,8 @@ data GhcjsApp route = GhcjsApp
5660
5761-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
5862--TODO: The frontend should be provided together with the asset paths so that this isn't so easily breakable; that will probably make this function obsolete
59−serveDefaultObeliskApp :: MonadSnap m => ([Text] -> m ()) -> Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
60−serveDefaultObeliskApp serveStaticAsset frontend = serveObeliskApp serveStaticAsset frontendApp
63+serveDefaultObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
64+serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
6165 where frontendApp = GhcjsApp
6266 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
6367 , _ghcjsApp_value = frontend
@@ -108,16 +112,17 @@ getRouteWith e = do
108112 pageName <- getPageName
109113 return $ tryDecode e pageName
110114
111−serveObeliskApp :: MonadSnap m => ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
112−serveObeliskApp serveStaticAsset frontendApp = \case
113− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
115+serveObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
116+serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
117+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
114118 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
115119 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
116− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
120+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
117121 ResourceRoute_JSaddleWarp :=> Identity _ -> do
118122 let msg = "Error: Obelisk.Backend received jsaddle request"
119123 liftIO $ putStrLn $ T.unpack msg
120124 writeText msg
125+ ResourceRoute_Version :=> Identity () -> doNotCache >> serveFileIfExistsAs "text/plain" "version"
121126
122127serveStaticAssets :: MonadSnap m => StaticAssets -> [Text] -> m ()
123128serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
@@ -132,11 +137,15 @@ data GhcjsAppRoute :: (* -> *) -> * -> * where
132137 GhcjsAppRoute_App :: appRouteComponent a -> GhcjsAppRoute appRouteComponent a
133138 GhcjsAppRoute_Resource :: GhcjsAppRoute appRouteComponent [Text]
134139
140+staticRenderContentType :: ByteString
141+staticRenderContentType = "text/html; charset=utf-8"
142+
135143--TODO: Don't assume we're being served at "/"
136−serveGhcjsApp :: MonadSnap m => GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
137−serveGhcjsApp app = \case
138− GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest ->
139− writeBS <=< liftIO $ renderGhcjsFrontend (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
144+serveGhcjsApp :: MonadSnap m => (R appRouteComponent -> Text) -> GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
145+serveGhcjsApp urlEnc app = \case
146+ GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
147+ modifyResponse $ setContentType staticRenderContentType
148+ writeBS <=< liftIO $ renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
140149 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
141150
142151runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
@@ -147,10 +156,20 @@ runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend
147156 getRouteWith validFullEncoder >>= \case
148157 Identity r -> case r of
149158 InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
150− InR obeliskRoute :=> Identity a -> serveDefaultObeliskApp (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
159+ InR obeliskRoute :=> Identity a ->
160+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
161+
162+mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
163+mkRouteToUrl validFullEncoder =
164+ let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
165+ in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
166+
151167
152−renderGhcjsFrontend :: route -> Frontend route -> IO ByteString
153−renderGhcjsFrontend route f = do
168+renderGhcjsFrontend :: MonadIO m => (route -> Text) -> route -> Frontend route -> m ByteString
169+renderGhcjsFrontend urlEnc route f = do
154170 let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
171+ ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
155172 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
156− renderFrontendHtml route (_frontend_head f >> injectExecutableConfigs >> baseTag) (_frontend_body f >> ghcjsScript)
173+ liftIO $ renderFrontendHtml urlEnc route
174+ (_frontend_head f >> injectExecutableConfigs >> baseTag >> ghcjsPreload)
175+ (_frontend_body f >> ghcjsScript)
lib/command/src/Obelisk/Command/Deploy.hs+138−83
@@ -3,22 +3,30 @@
33{-# LANGUAGE OverloadedStrings #-}
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE ViewPatterns #-}
6+{-# LANGUAGE DeriveGeneric #-}
67module Obelisk.Command.Deploy where
78
89import Control.Lens
910import Control.Monad
10−import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try)
11+import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try, bracket_)
1112import Control.Monad.IO.Class (liftIO)
13+import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)
1214import Data.Bits
15+import qualified Data.ByteString.Lazy as BSL
1316import Data.Default
1417import Data.List (isSuffixOf)
18+import Data.Map (Map)
1519import qualified Data.Map as Map
1620import Data.Maybe
21+import Data.Text (Text)
22+import qualified Data.Set as Set
1723import qualified Data.Text as T
1824import qualified Data.Text.IO as T
25+import GHC.Generics
1926import System.Directory
2027import System.Environment (getEnvironment)
2128import System.FilePath
29+import System.IO
2230import System.Posix.Files
2331import System.Process (delegate_ctlc, env, proc, readCreateProcess, cwd)
2432import Text.URI (URI)
@@ -83,71 +91,65 @@ setupObeliskImpl deployDir = do
8391
8492deployPush :: MonadObelisk m => FilePath -> m [String] -> m ()
8593deployPush deployPath getNixBuilders = do
86− host <- readDeployConfig deployPath "backend_hosts"
94+ hosts <- Set.fromList . filter (/= mempty) . lines <$> readDeployConfig deployPath "backend_hosts"
8795 adminEmail <- readDeployConfig deployPath "admin_email"
8896 enableHttps <- read <$> readDeployConfig deployPath "enable_https"
8997 route <- readDeployConfig deployPath $ "config" </> "common" </> "route"
9098 routeHost <- getHostFromRoute enableHttps route
9199 let srcPath = deployPath </> "src"
92− build = do
93− builders <- getNixBuilders
94− buildOutput <- nixBuild $ def
95− { _nixBuildConfig_target = Target
96− { _target_path = srcPath
97− , _target_attr = Just "server.system"
98− }
99− , _nixBuildConfig_outLink = OutLink_None
100− , _nixBuildConfig_args =
101− [ strArg "hostName" host
102− , strArg "adminEmail" adminEmail
103− , strArg "routeHost" routeHost
104− , boolArg "enableHttps" enableHttps
105− , rawArg "config" $ deployPath </> "config"
106− ]
107− , _nixBuildConfig_builders = builders
108− }
109− return $ listToMaybe $ lines buildOutput
110− result <- readThunk srcPath >>= \case
111− Right (ThunkData_Packed _) ->
112− build
100+ thunkPtr <- readThunk srcPath >>= \case
101+ Right (ThunkData_Packed ptr) -> return ptr
113102 Right (ThunkData_Checkout _) -> do
114103 checkGitCleanStatus srcPath True >>= \case
115− True -> do
116− result <- build
117− packThunk srcPath
118− return result
119− False -> do
120− _ <- failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
121− return Nothing
122− _ -> return Nothing
123− forM_ result $ \res -> do
124− let knownHostsPath = deployPath </> "backend_known_hosts"
125− sshOpts = sshArgs knownHostsPath (deployPath </> "ssh_key") False
126− deployAndSwitch outputPath = do
127− withSpinner "Uploading closure" $ do
128− callProcess'
129− (Map.fromList [("NIX_SSHOPTS", unwords sshOpts)])
130− "nix-copy-closure" ["-v", "--to", "root@" <> host, "--gzip", outputPath]
131−
132− withSpinner "Switching to new configuration" $ do
133− callProcessAndLogOutput (Notice, Warning) $
134− proc "ssh" $ sshOpts <>
135− [ "root@" <> host
136− , unwords
137− [ "nix-env -p /nix/var/nix/profiles/system --set " <> outputPath
138− , "&&"
139− , "/nix/var/nix/profiles/system/bin/switch-to-configuration switch"
140− ]
141− ]
142− deployAndSwitch res
143− isClean <- checkGitCleanStatus deployPath True
144− when (not isClean) $ do
145− withSpinner "Commiting changes to Git" $ do
146− callProcessAndLogOutput (Debug, Error) $ proc "git"
147− ["-C", deployPath, "add", "."]
148− callProcessAndLogOutput (Debug, Error) $ proc "git"
149− ["-C", deployPath, "commit", "-m", "New deployment"]
150− putLog Notice $ "Deployed => " <> T.pack route
104+ True -> packThunk srcPath
105+ False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
106+ Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
107+ let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
108+ builders <- getNixBuilders
109+ buildOutputByHost <- ifor (Map.fromSet (const ()) hosts) $ \host () -> do
110+ --TODO: What does it mean if this returns more or less than 1 line of output?
111+ [result] <- fmap lines $ nixCmd $ NixCmd_Build $ def
112+ & nixCmdConfig_target .~ Target
113+ { _target_path = Just srcPath
114+ , _target_attr = Just "server.system"
115+ , _target_expr = Nothing
116+ }
117+ & nixBuildConfig_outLink .~ OutLink_None
118+ & nixCmdConfig_args .~
119+ [ strArg "hostName" host
120+ , strArg "adminEmail" adminEmail
121+ , strArg "routeHost" routeHost
122+ , strArg "version" version
123+ , boolArg "enableHttps" enableHttps
124+ , rawArg "config" $ deployPath </> "config"
125+ ]
126+ & nixCmdConfig_builders .~ builders
127+ pure result
128+ let knownHostsPath = deployPath </> "backend_known_hosts"
129+ sshOpts = sshArgs knownHostsPath (deployPath </> "ssh_key") False
130+ withSpinner "Uploading closures" $ ifor_ buildOutputByHost $ \host outputPath -> do
131+ callProcess'
132+ (Map.fromList [("NIX_SSHOPTS", unwords sshOpts)])
133+ "nix-copy-closure" ["-v", "--to", "root@" <> host, "--gzip", outputPath]
134+ --TODO: Create GC root so we're sure our closure won't go away during this time period
135+ withSpinner "Switching to new configuration" $ ifor_ buildOutputByHost $ \host outputPath -> do
136+ callProcessAndLogOutput (Notice, Warning) $
137+ proc "ssh" $ sshOpts <>
138+ [ "root@" <> host
139+ , unwords
140+ [ "nix-env -p /nix/var/nix/profiles/system --set " <> outputPath
141+ , "&&"
142+ , "/nix/var/nix/profiles/system/bin/switch-to-configuration switch"
143+ ]
144+ ]
145+ isClean <- checkGitCleanStatus deployPath True
146+ when (not isClean) $ do
147+ withSpinner "Commiting changes to Git" $ do
148+ callProcessAndLogOutput (Debug, Error) $ proc "git"
149+ ["-C", deployPath, "add", "."]
150+ callProcessAndLogOutput (Debug, Error) $ proc "git"
151+ ["-C", deployPath, "commit", "-m", "New deployment"]
152+ putLog Notice $ "Deployed => " <> T.pack route
151153 where
152154 callProcess' envMap cmd args = do
153155 processEnv <- Map.toList . (envMap <>) . Map.fromList <$> liftIO getEnvironment
@@ -157,36 +159,88 @@ deployPush deployPath getNixBuilders = do
157159deployUpdate :: MonadObelisk m => FilePath -> m ()
158160deployUpdate deployPath = updateThunkToLatest $ deployPath </> "src"
159161
162+keytoolToAndroidConfig :: KeytoolConfig -> Map Text NValue
163+keytoolToAndroidConfig conf = Map.fromList
164+ [ ("storeFile", NValue_Path $ _keytoolConfig_keystore conf)
165+ , ("storePassword", NValue_Text $ T.pack $ _keytoolConfig_storepass conf)
166+ , ("keyAlias", NValue_Text $ T.pack $ _keytoolConfig_alias conf)
167+ , ("keyPassword", NValue_Text $ T.pack $ _keytoolConfig_keypass conf)
168+ ]
169+
170+
160171deployMobile :: MonadObelisk m => String -> [String] -> m ()
161172deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
162173 let srcDir = root </> "src"
163174 exists <- liftIO $ doesDirectoryExist srcDir
164175 unless exists $ failWith "ob test should be run inside of a deploy directory"
165176 when (platform == "android") $ do
166− let signKeyDir = root </> "backend"
167− -- check to see if config/android/...jks exist
168− androidDirExist <- liftIO $ doesDirectoryExist srcDir
169− -- if it doesn't, create the directory and...
170− -- keyFilename <- liftIO getLine
171− liftIO $ print $ ("Enter desired APK key alias:" :: String)
172− alias <- liftIO getLine
173− liftIO $ print $ ("starting keytool..." :: String)
174− -- ... run keytool command if there isn't a file (what should the name of the file be?) located at config/android
175− searchResults <- liftIO $ findFileWith (\fp -> return $ isSuffixOf ".jks" fp) [signKeyDir] "signKey"
176− -- TODO: How do I allow user to use prompts within keytool?
177− case searchResults of
178− Nothing -> do
179− let impl = toImplDir "."
180− callProcessAndLogOutput (Notice,Notice) (proc "nix-shell"
181− [ "-E"
182− , "with (import " <> impl <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
183− , "--run"
184− , "keytool -genkey -v -keystore " <> (signKeyDir </> "obeliskkeystore.jks") <> " -keyalg RSA -keysize 2048 -validity 10000 -alias " <> alias
185− ]) { cwd = Just root }
186− _ -> return ()
187− result <- nixBuildAttrWithCache srcDir $ platform <> ".frontend"
188− putLog Notice $ "Your recently built android apk can be found at the following path: " <> (show result)
189− callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
177+ let keystorePath = root </> "android_keystore.jks"
178+ keytoolConfPath = root </> "android_keytool_config.json"
179+ hasKeystore <- liftIO $ doesFileExist keystorePath
180+ when (not hasKeystore) $ do
181+ -- TODO log instructions for how to modify the keystore
182+ putLog Notice $ "Creating keystore: " <> T.pack keystorePath
183+ putLog Notice "Enter a keystore password: "
184+ keyStorePassword <- liftIO $ withEcho False getLine
185+ putLog Notice "Re-enter the keystore password: "
186+ keyStorePassword' <- liftIO $ withEcho False getLine
187+ unless (keyStorePassword' == keyStorePassword) $ failWith "passwords do not match"
188+ let keyToolConf = KeytoolConfig
189+ { _keytoolConfig_keystore = keystorePath
190+ , _keytoolConfig_alias = "obelisk"
191+ , _keytoolConfig_storepass = keyStorePassword
192+ , _keytoolConfig_keypass = keyStorePassword
193+ , _keytoolConfig_dname = "CN=example.com, OU=engineering, O=obelisk, L=LinuxLand, S=NY, C=US" -- TODO Read these from config?
194+ }
195+ createKeystore root $ keyToolConf
196+ liftIO $ BSL.writeFile keytoolConfPath $ encode keyToolConf
197+ checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
198+ unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
199+ keytoolConfContents <- liftIO $ BSL.readFile keytoolConfPath
200+ liftIO $ putStrLn $ show keytoolConfContents
201+ releaseKey <- case eitherDecode keytoolConfContents of
202+ Left err -> failWith $ T.pack err
203+ Right conf -> return $ renderAttrset $ keytoolToAndroidConfig conf
204+ result <- nixCmd $ NixCmd_Build $ def
205+ & nixBuildConfig_outLink .~ OutLink_None
206+ & nixCmdConfig_target .~ Target
207+ { _target_path = Nothing
208+ , _target_attr = Nothing
209+ , _target_expr = Just $ "with (import " <> srcDir <> " {}); " <> platform <> ".frontend.override (drv: { releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> T.unpack releaseKey <> "; })"
210+ }
211+ putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> (show result)
212+ callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
213+ where
214+ withEcho showEcho f = do
215+ prevEcho <- hGetEcho stdin
216+ bracket_ (hSetEcho stdin showEcho) (hSetEcho stdin prevEcho) f
217+
218+data KeytoolConfig = KeytoolConfig
219+ { _keytoolConfig_keystore :: FilePath
220+ , _keytoolConfig_alias :: String
221+ , _keytoolConfig_storepass :: String
222+ , _keytoolConfig_keypass :: String
223+ , _keytoolConfig_dname :: String
224+ } deriving (Show, Generic)
225+
226+instance FromJSON KeytoolConfig
227+instance ToJSON KeytoolConfig
228+
229+createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
230+createKeystore root config = do
231+ let expr = "with (import " <> toImplDir root <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
232+ callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
233+ where
234+ keytoolCmd = unwords $ "keytool" :
235+ [ "-genkeypair -noprompt"
236+ , "-keystore", _keytoolConfig_keystore config
237+ , "-keyalg RSA -keysize 2048 -validity 10000"
238+ , "-storepass", _keytoolConfig_storepass config
239+ , "-alias", _keytoolConfig_alias config
240+ , "-keypass", _keytoolConfig_keypass config
241+ , "-dname ", quote $ _keytoolConfig_dname config
242+ ]
243+ quote x = "\"" <> x <> "\""
190244
191245-- | Simplified deployment configuration mechanism. At one point we may revisit this.
192246writeDeployConfig :: MonadObelisk m => FilePath -> FilePath -> String -> m ()
@@ -201,6 +255,7 @@ verifyHostKey knownHostsPath keyPath hostName =
201255 callProcessAndLogOutput (Notice, Warning) $ proc "ssh" $
202256 sshArgs knownHostsPath keyPath True <>
203257 [ "root@" <> hostName
258+ , "-o", "NumberOfPasswordPrompts=0"
204259 , "exit"
205260 ]
206261
lib/command/src/Obelisk/Command/Nix.hs+132−40
@@ -6,53 +6,60 @@
66{-# LANGUAGE OverloadedStrings #-}
77{-# LANGUAGE Rank2Types #-}
88{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TemplateHaskell #-}
910{-# LANGUAGE TypeApplications #-}
1011module Obelisk.Command.Nix
11− ( nixBuild
12+ ( nixCmd
13+ , NixCmd (..)
14+ , nixCmdConfig_target
15+ , nixCmdConfig_args
16+ , nixCmdConfig_builders
1217 , NixBuildConfig (..)
18+ , nixBuildConfig_outLink
19+ , NixInstantiateConfig (..)
20+ , nixInstantiateConfig_eval
21+ , NixCommonConfig (..)
1322 , Target (..)
1423 , OutLink (..)
1524 , Arg (..)
1625 , boolArg
1726 , rawArg
1827 , strArg
28+ , NValue (..)
29+ , renderNValue
30+ , renderAttrset
1931 ) where
2032
21−--import Control.Monad.Catch (catch, throwM)
33+import Control.Monad (guard)
2234import Control.Lens
23−import Control.Monad.Except (ExceptT, runExceptT, throwError)
24−import Control.Monad.IO.Class (liftIO)
35+
2536import Data.Bool (bool)
2637import Data.Default
2738import Data.List (intercalate)
39+import Data.Map (Map)
40+import qualified Data.Map as M
41+import Data.Maybe
2842import Data.Monoid ((<>))
43+import Data.Text (Text)
2944import qualified Data.Text as T
30−import System.Directory
31−import System.Environment
3245import System.Process (proc)
3346
34−import Obelisk.App (MonadInfallibleObelisk, MonadObelisk, ObeliskError (..))
47+import Obelisk.App (MonadObelisk)
3548import Obelisk.CliApp
3649
37−-- | Where to put nix-build output
38−data OutLink
39− = OutLink_Default
40− | OutLink_None
41− | OutLink_IndirectRoot FilePath
42−
43−instance Default OutLink where
44− def = OutLink_Default
45−
4650-- | What to build
4751data Target = Target
48− { _target_path :: FilePath
52+ { _target_path :: Maybe FilePath
4953 , _target_attr :: Maybe String
54+ , _target_expr :: Maybe String
5055 }
56+makeClassy ''Target
5157
5258instance Default Target where
5359 def = Target
54− { _target_path = "."
60+ { _target_path = Just "."
5561 , _target_attr = Nothing
62+ , _target_expr = Nothing
5663 }
5764
5865data Arg
@@ -74,36 +81,121 @@ cliFromArgs = concatMap $ \case
7481 Arg_Str k v -> ["--argstr", k, v]
7582 Arg_Expr k v -> ["--arg", k, v]
7683
77−data NixBuildConfig = NixBuildConfig
78− { _nixBuildConfig_target :: Target
79− , _nixBuildConfig_outLink :: OutLink
80− , _nixBuildConfig_args :: [Arg]
81− , _nixBuildConfig_builders :: [String]
84+data NixCommonConfig = NixCommonConfig
85+ { _nixCmdConfig_target :: Target
86+ , _nixCmdConfig_args :: [Arg]
87+ , _nixCmdConfig_builders :: [String]
8288 }
89+makeClassy ''NixCommonConfig
8390
84−instance Default NixBuildConfig where
85− def = NixBuildConfig def def mempty mempty
91+instance Default NixCommonConfig where
92+ def = NixCommonConfig def mempty mempty
8693
87−nixBuild :: MonadObelisk m => NixBuildConfig -> m FilePath
88−nixBuild cfg = withSpinner' ("Running nix-build on " <> desc) (Just $ const $ "Built " <> desc) $ do
89− output <- readProcessAndLogStderr Debug $ proc "nix-build" $ mconcat
90− [[path], attrArg, args, outLink, buildersArg]
91− -- Remove final newline that Nix appends
92− Just (outPath, '\n') <- pure $ T.unsnoc output
93− pure $ T.unpack outPath
94+runNixCommonConfig :: NixCommonConfig -> [FilePath]
95+runNixCommonConfig cfg = mconcat [maybeToList path, attrArg, exprArg, args, buildersArg]
9496 where
95− path = _target_path $ _nixBuildConfig_target cfg
96− attr = _target_attr $ _nixBuildConfig_target cfg
97+ path = _target_path $ _nixCmdConfig_target cfg
98+ attr = _target_attr $ _nixCmdConfig_target cfg
99+ expr = _target_expr $ _nixCmdConfig_target cfg
97100 attrArg = case attr of
98101 Nothing -> []
99102 Just a -> ["-A", a]
100− args = cliFromArgs $ _nixBuildConfig_args cfg
101− outLink = case _nixBuildConfig_outLink cfg of
103+ exprArg = case expr of
104+ Nothing -> []
105+ Just a -> ["-E", a]
106+ args = cliFromArgs $ _nixCmdConfig_args cfg
107+ buildersArg = case _nixCmdConfig_builders cfg of
108+ [] -> []
109+ builders -> ["--builders", intercalate ";" builders]
110+
111+-- | Where to put nix-build output
112+data OutLink
113+ = OutLink_Default
114+ | OutLink_None
115+ | OutLink_IndirectRoot FilePath
116+
117+instance Default OutLink where
118+ def = OutLink_Default
119+
120+data NixBuildConfig = NixBuildConfig
121+ { _nixBuildConfig_common :: NixCommonConfig
122+ , _nixBuildConfig_outLink :: OutLink
123+ }
124+makeLenses ''NixBuildConfig
125+
126+instance HasNixCommonConfig NixBuildConfig where
127+ nixCommonConfig = nixBuildConfig_common
128+
129+instance Default NixBuildConfig where
130+ def = NixBuildConfig def def
131+
132+runNixBuildConfig :: NixBuildConfig -> [FilePath]
133+runNixBuildConfig cfg = mconcat
134+ [ runNixCommonConfig $ cfg ^. nixCommonConfig
135+ , case _nixBuildConfig_outLink cfg of
102136 OutLink_Default -> []
103137 OutLink_None -> ["--no-out-link"]
104138 OutLink_IndirectRoot l -> ["--out-link", l]
105− desc = T.pack $ path <> maybe "" (\a -> " [" <> a <> "]") attr
106− buildersArg = case _nixBuildConfig_builders cfg of
107− [] -> []
108− builders -> ["--builders", intercalate ";" builders]
139+ ]
140+
141+data NixInstantiateConfig = NixInstantiateConfig
142+ { _nixInstantiateConfig_common :: NixCommonConfig
143+ , _nixInstantiateConfig_eval :: Bool
144+ }
145+makeLenses ''NixInstantiateConfig
146+
147+instance HasNixCommonConfig NixInstantiateConfig where
148+ nixCommonConfig = nixInstantiateConfig_common
149+
150+instance Default NixInstantiateConfig where
151+ def = NixInstantiateConfig def False
152+
153+runNixInstantiateConfig :: NixInstantiateConfig -> [FilePath]
154+runNixInstantiateConfig cfg = mconcat
155+ [ runNixCommonConfig $ cfg ^. nixCommonConfig
156+ , "--eval" <$ guard (_nixInstantiateConfig_eval cfg)
157+ ]
158+
159+data NixCmd
160+ = NixCmd_Build NixBuildConfig
161+ | NixCmd_Instantiate NixInstantiateConfig
162+
163+instance Default NixCmd where
164+ def = NixCmd_Build def
165+
166+nixCmd :: MonadObelisk m => NixCmd -> m FilePath
167+nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
168+ output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) $ options
169+ -- Remove final newline that Nix appends
170+ Just (outPath, '\n') <- pure $ T.unsnoc output
171+ pure $ T.unpack outPath
172+ where
173+ (cmd, options, commonCfg) = case cmdCfg of
174+ NixCmd_Build cfg' ->
175+ ( "nix-build"
176+ , runNixBuildConfig cfg'
177+ , cfg' ^. nixCommonConfig
178+ )
179+ NixCmd_Instantiate cfg' ->
180+ ( "nix-instantiate"
181+ , runNixInstantiateConfig cfg'
182+ , cfg' ^. nixCommonConfig
183+ )
184+ path = commonCfg ^. nixCmdConfig_target . target_path
185+ desc = T.pack $ mconcat $ catMaybes
186+ [ (" on " <>) <$> path
187+ , (\a -> " [" <> a <> "]") <$> (commonCfg ^. nixCmdConfig_target . target_attr)
188+ ]
189+
190+data NValue
191+ = NValue_Text Text
192+ | NValue_Path FilePath
193+
194+renderNValue :: NValue -> Text
195+renderNValue = \case
196+ NValue_Text x -> "\"" <> x <> "\""
197+ NValue_Path p -> T.pack p
198+
199+renderAttrset :: Map Text NValue -> Text
200+renderAttrset m = "{ " <> T.unwords (map (\(k,v) -> k <> " = " <> renderNValue v <> ";") $ M.toList m) <> " }"
109201
lib/command/src/Obelisk/Command/Project.hs+1−1
@@ -145,7 +145,7 @@ findProjectRoot target = do
145145 (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat myUid) []
146146 return result
147147
148−withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m ()) -> m ()
148+withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
149149withProjectRoot target f = findProjectRoot target >>= \case
150150 Nothing -> failWith "Must be used inside of an Obelisk project"
151151 Just root -> f root
lib/command/src/Obelisk/Command/Run.hs+20−5
@@ -26,16 +26,19 @@ import Hpack.Config
2626import Hpack.Render
2727import Hpack.Yaml
2828import Language.Haskell.Extension
29−import Network.Socket
29+import Network.Socket hiding (Debug)
3030import System.Directory
3131import System.FilePath
32+import System.Process (proc)
3233import System.IO.Temp (withSystemTempDirectory)
3334import Data.ByteString (ByteString)
3435
3536import Obelisk.App (MonadObelisk, ObeliskT)
36−import Obelisk.CliApp (CliT (..), HasCliConfig, Severity (..), callCommand, failWith, getCliConfig, putLog,
37− runCli)
38−import Obelisk.Command.Project (inProjectShell)
37+import Obelisk.CliApp
38+ ( CliT (..), HasCliConfig, Severity (..)
39+ , callCommand, failWith, getCliConfig, putLog
40+ , readProcessAndLogStderr, runCli)
41+import Obelisk.Command.Project (inProjectShell, withProjectRoot)
3942
4043data CabalPackageInfo = CabalPackageInfo
4144 { _cabalPackageInfo_packageRoot :: FilePath
@@ -51,7 +54,19 @@ run = do
5154 pkgs <- getLocalPkgs
5255 withGhciScript pkgs $ \dotGhciPath -> do
5356 freePort <- getFreePort
54− runGhcid dotGhciPath $ Just $ unwords ["run", show freePort, "(runServeAsset Obelisk.Generated.Static.staticRootPath)", "Backend.backend", "Frontend.frontend"]
57+ assets <- withProjectRoot "." $ \root ->
58+ -- `--raw` is not available with old nix-instantiate. It drops quotation
59+ -- marks and trailing newline, so is very convenient for shelling out.
60+ readProcessAndLogStderr Debug $
61+ proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
62+ putLog Debug $ "Assets impurely loaded from: " <> assets
63+ runGhcid dotGhciPath $ Just $ unwords
64+ [ "run"
65+ , show freePort
66+ , "(runServeAsset " ++ show assets ++ ")"
67+ , "Backend.backend"
68+ , "Frontend.frontend"
69+ ]
5570
5671runRepl :: MonadObelisk m => m ()
5772runRepl = do
lib/command/src/Obelisk/Command/Thunk.hs+26−16
@@ -33,6 +33,7 @@ module Obelisk.Command.Thunk
3333
3434import Control.Applicative
3535import Control.Exception (displayException, try)
36+import qualified Control.Lens as Lens
3637import Control.Lens.Indexed hiding ((<.>))
3738import Control.Monad
3839import Control.Monad.Catch (handle)
@@ -501,13 +502,13 @@ nixBuildThunkAttrWithCache thunkDir attr = do
501502 Just c -> return c
502503 Nothing -> do
503504 putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
504− _ <- nixBuild $ def
505− { _nixBuildConfig_target = Target
506− { _target_path = thunkDir
505+ _ <- nixCmd $ NixCmd_Build$ def
506+ Lens.& nixBuildConfig_outLink Lens..~ OutLink_IndirectRoot cachePath
507+ Lens.& nixCmdConfig_target Lens..~ Target
508+ { _target_path = Just thunkDir
507509 , _target_attr = Just attr
510+ , _target_expr = Nothing
508511 }
509− , _nixBuildConfig_outLink = OutLink_IndirectRoot cachePath
510− }
511512 return cachePath
512513
513514-- | Build a nix attribute, and cache the result if possible
@@ -523,13 +524,13 @@ nixBuildAttrWithCache exprPath attr = do
523524 readThunk exprPath >>= \case
524525 -- Only packed thunks are cached. in particular, checkouts are not
525526 Right (ThunkData_Packed _) -> nixBuildThunkAttrWithCache exprPath attr
526− _ -> nixBuild $ def
527− { _nixBuildConfig_target = Target
528− { _target_path = exprPath
527+ _ -> nixCmd $ NixCmd_Build $ def
528+ Lens.& nixBuildConfig_outLink Lens..~ OutLink_None
529+ Lens.& nixCmdConfig_target Lens..~ Target
530+ { _target_path = Just exprPath
529531 , _target_attr = Just attr
532+ , _target_expr = Nothing
530533 }
531− , _nixBuildConfig_outLink = OutLink_None
532− }
533534
534535-- | Safely update thunk using a custom action
535536--
@@ -553,7 +554,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
553554 return tmpThunk
554555 Right _ -> failWith $ "Thunk is not packed"
555556 updateThunkFromTmp p' = do
556− packThunk' True p'
557+ _ <- packThunk' True p'
557558 callProcessAndLogOutput (Notice, Error) $
558559 proc "cp" ["-r", "-T", p', p]
559560
@@ -563,8 +564,16 @@ finalMsg noTrail s = if noTrail then Nothing else Just s
563564unpackThunk :: MonadObelisk m => FilePath -> m ()
564565unpackThunk = unpackThunk' False
565566
567+-- | Check that we are not somewhere inside the thunk directory
568+checkThunkDirectory :: MonadObelisk m => FilePath -> m ()
569+checkThunkDirectory thunkDir = do
570+ currentDir <- liftIO getCurrentDirectory
571+ thunkDir' <- liftIO $ canonicalizePath thunkDir
572+ when (thunkDir' `L.isInfixOf` currentDir) $
573+ failWith "Can't pack/unpack from within the thunk directory"
574+
566575unpackThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
567−unpackThunk' noTrail thunkDir = readThunk thunkDir >>= \case
576+unpackThunk' noTrail thunkDir = checkThunkDirectory thunkDir >> readThunk thunkDir >>= \case
568577 Left err -> failWith $ "thunk unpack: " <> T.pack (show err)
569578 --TODO: Overwrite option that rechecks out thunk; force option to do so even if working directory is dirty
570579 Right (ThunkData_Checkout _) -> failWith "thunk unpack: thunk is already unpacked"
@@ -597,11 +606,11 @@ unpackThunk' noTrail thunkDir = readThunk thunkDir >>= \case
597606
598607--TODO: add force mode to pack even if changes are present
599608--TODO: add a rollback mode to pack to the original thunk
600−packThunk :: MonadObelisk m => FilePath -> m ()
609+packThunk :: MonadObelisk m => FilePath -> m ThunkPtr
601610packThunk = packThunk' False
602611
603−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
604−packThunk' noTrail thunkDir = readThunk thunkDir >>= \case
612+packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
613+packThunk' noTrail thunkDir = checkThunkDirectory thunkDir >> readThunk thunkDir >>= \case
605614 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
606615 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
607616 Right (ThunkData_Checkout _) -> do
@@ -610,6 +619,7 @@ packThunk' noTrail thunkDir = readThunk thunkDir >>= \case
610619 thunkPtr <- getThunkPtr thunkDir
611620 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
612621 liftIO $ createThunk thunkDir thunkPtr
622+ pure thunkPtr
613623
614624getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
615625getThunkPtr = getThunkPtr' True
@@ -661,7 +671,7 @@ getThunkPtr' checkClean thunkDir = do
661671 let untrackedBranches = Map.keys errorMap
662672 when (not $ L.null untrackedBranches) $ failWith $ T.unlines $
663673 [ "thunk pack: Certain branches in the thunk have no upstream branch \
664− \set. This means don't know to check whether all your work is \
674+ \set. This means we don't know to check whether all your work is \
665675 \saved. The offending branches are:"
666676 , ""
667677 , T.unwords untrackedBranches
lib/frontend/src/Obelisk/Frontend.hs+11−8
@@ -32,6 +32,7 @@ import Data.Dependent.Sum (DSum (..))
3232import Data.Functor.Sum
3333import Data.IORef
3434import Data.Monoid ((<>))
35+import Data.Text (Text)
3536import GHCJS.DOM hiding (bracket, catch)
3637import GHCJS.DOM.Document
3738import GHCJS.DOM.Node
@@ -64,6 +65,7 @@ type ObeliskWidget t x route m =
6465 , PrimMonad m
6566 , Prerender x m
6667 , SetRoute t route m
68+ , RouteToUrl route m
6769 )
6870
6971data Frontend route = Frontend
@@ -143,22 +145,23 @@ runFrontend validFullEncoder frontend = do
143145 , MonadFix m
144146 , MonadFix (Performable m)
145147 )
146− => RoutedT t (R route) (SetRouteT t (R route) m) a
148+ => RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
147149 -> m a
148150 runMyRouteViewT = runRouteViewT ve
149151 runWithHeadAndBody $ \appendHead appendBody -> runMyRouteViewT $ do
150− mapRoutedT (mapSetRouteT appendHead) $ _frontend_head frontend
151− mapRoutedT (mapSetRouteT appendBody) $ _frontend_body frontend
152+ mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ _frontend_head frontend
153+ mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ _frontend_body frontend
152154
153155renderFrontendHtml
154156 :: (t ~ DomTimeline)
155− => r
156− -> RoutedT t r (SetRouteT t r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost)))) ()
157− -> RoutedT t r (SetRouteT t r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost)))) ()
157+ => (r' -> Text)
158+ -> r
159+ -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
160+ -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
158161 -> IO ByteString
159−renderFrontendHtml route headWidget bodyWidget = do
162+renderFrontendHtml urlEnc route headWidget bodyWidget = do
160163 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
161− html <- fmap snd $ renderStatic $ fmap fst $ runSetRouteT $ flip runRoutedT (pure route) $
164+ html <- fmap snd $ renderStatic $ fmap fst $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
162165 el "html" $ do
163166 el "head" headWidget
164167 el "body" bodyWidget
lib/route/src/Obelisk/Route.hs+2
@@ -721,6 +721,7 @@ data ResourceRoute :: * -> * where
721721 ResourceRoute_Static :: ResourceRoute [Text] -- This [Text] represents the *path in our static files directory*, not necessarily the URL path that the asset gets served at (although that will often be "/static/this/text/thing")
722722 ResourceRoute_Ghcjs :: ResourceRoute [Text]
723723 ResourceRoute_JSaddleWarp :: ResourceRoute (R JSaddleWarpRoute)
724+ ResourceRoute_Version :: ResourceRoute ()
724725
725726-- | If there are no additional backend routes in your app (i.e. ObeliskRoute gives you all the routes you need),
726727-- this constructs a suitable 'Encoder' to use for encoding routes to 'PageName's. If you do have additional backend routes,
@@ -757,6 +758,7 @@ resourceRouteSegment = \case
757758 ResourceRoute_Static -> PathSegment "static" $ pathOnlyEncoderIgnoringQuery
758759 ResourceRoute_Ghcjs -> PathSegment "ghcjs" $ pathOnlyEncoder
759760 ResourceRoute_JSaddleWarp -> PathSegment "jsaddle" $ jsaddleWarpRouteEncoder
761+ ResourceRoute_Version -> PathSegment "version" $ unitEncoder mempty
760762
761763data JSaddleWarpRoute :: * -> * where
762764 JSaddleWarpRoute_JavaScript :: JSaddleWarpRoute ()
lib/route/src/Obelisk/Route/Frontend.hs+108−2
@@ -41,6 +41,11 @@ module Obelisk.Route.Frontend
4141 , SetRoute(..)
4242 , runSetRouteT
4343 , mapSetRouteT
44+ , RouteToUrl(..)
45+ , RouteToUrlT(..)
46+ , runRouteToUrlT
47+ , mapRouteToUrlT
48+ , routeLink
4449 ) where
4550
4651import Prelude hiding ((.), id)
@@ -61,6 +66,8 @@ import Data.Constraint (Dict (..))
6166import Data.Dependent.Sum (DSum (..))
6267import Data.GADT.Compare
6368import Data.Monoid
69+import Data.Proxy
70+import Data.Text (Text)
6471import qualified Data.Text as T
6572import Data.Functor.Compose
6673import Reflex.Class
@@ -290,6 +297,85 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
290297 askQueryResult = lift askQueryResult
291298 queryIncremental = lift . queryIncremental
292299
300+class RouteToUrl r m | m -> r where
301+ askRouteToUrl :: m (r -> Text)
302+
303+newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
304+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument)
305+
306+runRouteToUrlT
307+ :: RouteToUrlT r m a
308+ -> (r -> Text)
309+ -> m a
310+runRouteToUrlT a = runReaderT (unRouteToUrlT a)
311+
312+mapRouteToUrlT :: (forall x. m x -> n x) -> RouteToUrlT r m a -> RouteToUrlT r n a
313+mapRouteToUrlT f (RouteToUrlT m) = RouteToUrlT $ mapReaderT f m
314+
315+instance Monad m => RouteToUrl r (RouteToUrlT r m) where
316+ askRouteToUrl = RouteToUrlT ask
317+
318+instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
319+ askRouteToUrl = lift askRouteToUrl
320+
321+instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
322+ askRouteToUrl = lift askRouteToUrl
323+
324+instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
325+ type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
326+ askJSContext = lift askJSContext
327+
328+instance Prerender js m => Prerender js (RouteToUrlT r m) where
329+ prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
330+
331+instance Requester t m => Requester t (RouteToUrlT r m) where
332+ type Request (RouteToUrlT r m) = Request m
333+ type Response (RouteToUrlT r m) = Response m
334+ requesting = RouteToUrlT . requesting
335+ requesting_ = RouteToUrlT . requesting_
336+
337+#ifndef ghcjs_HOST_OS
338+deriving instance MonadJSM m => MonadJSM (RouteToUrlT r m)
339+#endif
340+
341+instance PerformEvent t m => PerformEvent t (RouteToUrlT r m) where
342+ type Performable (RouteToUrlT r m) = Performable m
343+ performEvent = lift . performEvent
344+ performEvent_ = lift . performEvent_
345+
346+instance MonadRef m => MonadRef (RouteToUrlT r m) where
347+ type Ref (RouteToUrlT r m) = Ref m
348+ newRef = lift . newRef
349+ readRef = lift . readRef
350+ writeRef r = lift . writeRef r
351+
352+instance HasJS x m => HasJS x (RouteToUrlT r m) where
353+ type JSX (RouteToUrlT r m) = JSX m
354+ liftJS = lift . liftJS
355+
356+instance MonadTransControl (RouteToUrlT r) where
357+ type StT (RouteToUrlT r) a = StT (ReaderT (r -> Text)) a
358+ liftWith = defaultLiftWith RouteToUrlT unRouteToUrlT
359+ restoreT = defaultRestoreT RouteToUrlT
360+
361+instance PrimMonad m => PrimMonad (RouteToUrlT r m ) where
362+ type PrimState (RouteToUrlT r m) = PrimState m
363+ primitive = lift . primitive
364+
365+instance DomBuilder t m => DomBuilder t (RouteToUrlT r m) where
366+ type DomBuilderSpace (RouteToUrlT r m) = DomBuilderSpace m
367+
368+instance Adjustable t m => Adjustable t (RouteToUrlT r m) where
369+ runWithReplace a0 a' = RouteToUrlT $ runWithReplace (coerce a0) $ coerceEvent a'
370+ traverseIntMapWithKeyWithAdjust f a0 a' = RouteToUrlT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
371+ traverseDMapWithKeyWithAdjust f a0 a' = RouteToUrlT $ traverseDMapWithKeyWithAdjust (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
372+ traverseDMapWithKeyWithAdjustWithMove f a0 a' = RouteToUrlT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
373+
374+instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
375+ tellQueryIncremental = lift . tellQueryIncremental
376+ askQueryResult = lift askQueryResult
377+ queryIncremental = lift . queryIncremental
378+
293379runRouteViewT
294380 :: forall t m r a.
295381 ( TriggerEvent t m
@@ -300,7 +386,7 @@ runRouteViewT
300386 , MonadFix m
301387 )
302388 => (Encoder Identity Identity r PageName)
303− -> RoutedT t r (SetRouteT t r m) a
389+ -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
304390 -> m a
305391runRouteViewT routeEncoder a = do
306392 rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
@@ -313,7 +399,7 @@ runRouteViewT routeEncoder a = do
313399 where
314400 errorLeft (Left e) = error (T.unpack e)
315401 errorLeft (Right x) = x
316− (result, changeState) <- runSetRouteT $ runRoutedT a route
402+ (result, changeState) <- runRouteToUrlT (runSetRouteT $ runRoutedT a route) $ (\(p, q) -> T.pack $ p <> q) . encode theEncoder
317403 let f (currentHistoryState, oldRoute) change =
318404 let newRoute = appEndo change oldRoute
319405 (newPath, newQuery) = encode theEncoder newRoute
@@ -337,6 +423,26 @@ runRouteViewT routeEncoder a = do
337423 setState = attachWith f ((,) <$> current historyState <*> current route) changeState
338424 return result
339425
426+-- | A link widget that, when clicked, sets the route to the provided route. In non-javascript
427+-- contexts, this widget falls back to using @href@s to control navigation
428+routeLink
429+ :: forall t m a route.
430+ ( DomBuilder t m
431+ , RouteToUrl (R route) m
432+ , SetRoute t (R route) m
433+ )
434+ => R route -- ^ Target route
435+ -> m a -- ^ Child widget
436+ -> m a
437+routeLink r w = do
438+ enc <- askRouteToUrl
439+ let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
440+ & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
441+ & elementConfig_initialAttributes .~ "href" =: enc r
442+ (e, a) <- element "a" cfg w
443+ setRoute $ r <$ domEvent Click e
444+ return a
445+
340446-- On ios due to sandboxing when loading the page from a file adapt the
341447-- path to be based on the hash.
342448
lib/run/src/Obelisk/Run.hs+8−6
@@ -82,7 +82,8 @@ run port serveStaticAsset backend frontend = do
8282 getRouteWith validFullEncoder >>= \case
8383 Identity r -> case r of
8484 InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
85− InR obeliskRoute :=> Identity a -> serveDefaultObeliskApp serveStaticAsset frontend $ obeliskRoute :/ a
85+ InR obeliskRoute :=> Identity a ->
86+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
8687 let conf = defRunConfig { _runConfig_redirectPort = port }
8788 runWidget conf frontend validFullEncoder `finally` killThread backendTid
8889
@@ -142,14 +143,15 @@ obeliskApp opts frontend validFullEncoder uri backend = do
142143 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
143144 }
144145 InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
145− html <- renderJsaddleFrontend (appRouteComponent :/ appRouteRest) frontend
146− sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] $ BSLC.fromStrict html
146+ html <- renderJsaddleFrontend (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
147+ sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
147148 _ -> backend req sendResponse
148149
149−renderJsaddleFrontend :: route -> Frontend route -> IO ByteString
150−renderJsaddleFrontend r f =
150+renderJsaddleFrontend :: (route -> Text) -> route -> Frontend route -> IO ByteString
151+renderJsaddleFrontend urlEnc r f =
151152 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
152− in renderFrontendHtml r (_frontend_head f >> injectExecutableConfigs) (_frontend_body f >> jsaddleScript)
153+ jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
154+ in renderFrontendHtml urlEnc r (_frontend_head f >> injectExecutableConfigs >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
153155
154156-- | like 'bindPortTCP' but reconnects on exception
155157bindPortTCPRetry :: Settings
lib/selftest/src/Obelisk/SelfTest.hs+31−20
@@ -76,16 +76,19 @@ main = do
7676 let verbosity = bool silently verbosely isVerbose
7777 obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
7878 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
79+ [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
7980 withSystemTempDirectory "initCache" $ \initCache -> do
8081 -- Setup the ob init cache
8182 void . shellyOb verbosity $ chdir (fromString initCache) $ do
8283 run_ "ob" ["init"]
8384 run_ "git" ["init"]
84− hspec $ do
85+ hspec $ parallel $ do
8586 let shelly_ = void . shellyOb verbosity
8687
8788 inTmp :: (Shelly.FilePath -> Sh a) -> IO ()
88− inTmp f = shelly_ . withSystemTempDirectory "test" $ (chdir <*> f) . fromString
89+ inTmp f = withTmp (chdir <*> f)
90+
91+ withTmp f = shelly_ . withSystemTempDirectory "test" $ f . fromString
8992
9093 inTmpObInit f = inTmp $ \dir -> do
9194 run_ "cp" ["-a", fromString $ initCache <> "/.", toTextIgnore dir]
@@ -108,7 +111,7 @@ main = do
108111
109112 diff a b = run "git" ["diff", a, b]
110113
111− describe "ob init" $ do
114+ describe "ob init" $ parallel $ do
112115 it "works with default impl" $ inTmp $ \_ -> run "ob" ["init"]
113116 it "works with master branch impl" $ inTmp $ \_ -> run "ob" ["init", "--branch", "master"]
114117 it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", obeliskImpl]
@@ -128,20 +131,20 @@ main = do
128131
129132 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
130133 -- when not run by 'nix-build --attr selftest'
131− describe "ob run" $ do
134+ describe "ob run" $ parallel $ do
132135 it "works in root directory" $ inTmpObInit $ \_ -> do
133− testObRunInDir Nothing httpManager
136+ testObRunInDir p0 p1 Nothing httpManager
134137 it "works in sub directory" $ inTmpObInit $ \_ -> do
135− testObRunInDir (Just "frontend") httpManager
138+ testObRunInDir p2 p3 (Just "frontend") httpManager
136139
137− describe "obelisk project" $ do
138− it "can build obelisk command" $ shelly_ $ run "nix-build" ["-A", "command" , obeliskImpl]
139− it "can build obelisk skeleton" $ shelly_ $ run "nix-build" ["-A", "skeleton", obeliskImpl]
140− it "can build obelisk shell" $ shelly_ $ run "nix-build" ["-A", "shell", obeliskImpl]
140+ describe "obelisk project" $ parallel $ do
141+ it "can build obelisk command" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "command" , obeliskImpl]
142+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "skeleton", obeliskImpl]
143+ it "can build obelisk shell" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "shell", obeliskImpl]
141144 -- See https://github.com/obsidiansystems/obelisk/issues/101
142145 -- it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
143146
144− describe "blank initialized project" $ do
147+ describe "blank initialized project" $ parallel $ do
145148
146149 it "can build ghc.backend" $ inTmpObInit $ \_ -> do
147150 run "nix-build" ["--no-out-link", "-A", "ghc.backend"]
@@ -167,7 +170,7 @@ main = do
167170 uu <- update
168171 assertRevEQ u uu
169172
170− describe "ob thunk pack/unpack" $ do
173+ describe "ob thunk pack/unpack" $ parallel $ do
171174 it "has thunk pack and unpack inverses" $ inTmpObInit $ \_ -> do
172175
173176 e <- commitAll
@@ -180,6 +183,14 @@ main = do
180183 assertRevEQ eu eupu
181184 assertRevNE e eu
182185
186+ it "unpacks the correct branch" $ withTmp $ \dir -> do
187+ let branch = "master"
188+ run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
189+ run_ "ob" ["thunk", "pack", toTextIgnore dir]
190+ run_ "ob" ["thunk", "unpack", toTextIgnore dir]
191+ branch' <- chdir dir $ run "git" ["rev-parse", "--abbrev-ref", "HEAD"]
192+ liftIO $ assertEqual "" branch (T.strip branch')
193+
183194 it "can pack and unpack plain git repos" $ do
184195 shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
185196 let repo = toTextIgnore $ dir </> ("repo" :: String)
@@ -203,14 +214,13 @@ main = do
203214 testThunkPack (dir </> thunk)
204215
205216-- | Run `ob run` in the given directory (maximum of one level deep)
206−testObRunInDir :: Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
207−testObRunInDir mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
217+testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
218+testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
208219 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
209− writefile "config/common/route" . uri =<< liftIO getFreePort -- Use a free port initially
220+ writefile "config/common/route" $ uri p0
210221 maybe id chdir mdir $ runHandle "ob" ["run"] $ \stdout -> do
211− freePort <- liftIO getFreePort
212222 firstUri <- handleObRunStdout httpManager stdout
213− let newUri = uri freePort
223+ let newUri = uri p1
214224 when (firstUri == newUri) $ errorExit $
215225 "Startup URI (" <> firstUri <> ") is the same as test URI (" <> newUri <> ")"
216226 maybe id (\_ -> chdir "..") mdir $ alterRouteTo newUri stdout
@@ -285,10 +295,11 @@ obRunCheck httpManager _stdout frontendUri = do
285295 req frontendUri >>= \r -> when (HTTP.responseStatus r /= HTTP.ok200) $ errorExit $
286296 "Request to frontend server failed: " <> T.pack (show r)
287297
288−getFreePort :: IO Socket.PortNumber
289−getFreePort = Socket.withSocketsDo $ do
298+getFreePorts :: Int -> IO [Socket.PortNumber]
299+getFreePorts 0 = pure []
300+getFreePorts n = Socket.withSocketsDo $ do
290301 addr:_ <- Socket.getAddrInfo (Just Socket.defaultHints) (Just "127.0.0.1") (Just "0")
291− bracket (open addr) Socket.close Socket.socketPort
302+ bracket (open addr) Socket.close $ \s -> (:) <$> Socket.socketPort s <*> getFreePorts (n - 1)
292303 where
293304 open addr = do
294305 sock <- Socket.socket (Socket.addrFamily addr) (Socket.addrSocketType addr) (Socket.addrProtocol addr)
lib/snap/obelisk-snap.cabaldeleted−40
@@ -1,40 +0,0 @@
1−Name: obelisk-snap
2−Version: 0.1
3−Synopsis: Project Synopsis Here
4−Description: Project Description Here
5−License: BSD3
6−Author: Obsidian Systems
7−Maintainer: maintainer@obsidian.systems
8−Stability: Experimental
9−Category: Web
10−Build-type: Simple
11−Cabal-version: >=1.2
12−
13−library
14− hs-source-dirs: src
15−
16− build-depends:
17− base,
18− bytestring,
19− data-default,
20− diagrams-lib,
21− diagrams-svg,
22− filepath,
23− lens,
24− lucid,
25− monad-control,
26− obelisk-asset-serve-snap,
27− obelisk-snap-extras,
28− semigroups,
29− raw-strings-qq,
30− snap,
31− snap-core,
32− svg-builder,
33− text,
34− transformers-base
35−
36− exposed-modules:
37− Obelisk.Snap
38−
39− other-extensions: TemplateHaskell
40− ghc-options: -threaded -Wall -fno-warn-unused-do-bind -fwarn-tabs -funbox-strict-fields -O2 -fprof-auto-calls -rtsopts
lib/snap/src/Obelisk/Snap.hsdeleted−99
@@ -1,99 +0,0 @@
1−{-# LANGUAGE TemplateHaskell #-}
2−{-# LANGUAGE QuasiQuotes #-}
3−{-# LANGUAGE OverloadedStrings #-}
4−
5−module Obelisk.Snap where
6−
7−import Obelisk.Asset.Serve.Snap
8−import Obelisk.Snap.Extras
9−
10−import Snap
11−
12−import Control.Lens
13−import Data.ByteString (ByteString)
14−import Data.Default
15−import Data.Monoid
16−import qualified Data.Text as T
17−import Lucid
18−import System.FilePath
19−import Control.Monad.IO.Class
20−
21−-- Data type for web app configuration
22−data AppConfig m
23− = AppConfig { _appConfig_initialBody :: m ByteString
24− , _appConfig_initialHead :: ByteString
25− , _appConfig_serveJsexe :: Bool
26− , _appConfig_jsexe :: FilePath
27− }
28−
29−-- Default instance for app configuration
30−instance Monad m => Default (AppConfig m) where
31− def = AppConfig { _appConfig_initialBody = return mempty
32− , _appConfig_initialHead = mempty
33− , _appConfig_serveJsexe = True
34− , _appConfig_jsexe = "frontend.jsexe"
35− }
36−
37−{- | Takes a location, app, and an app configuration as arguements and
38− routes the request to the correct handler. If "appConfig_serveJsexe"
39− returns "True", additional js paths and asset paths will be appended to
40− handler. Otherwise, it will return "404 Not Found" -}
41−serveAppAt :: MonadSnap m => ByteString -> FilePath -> AppConfig m -> m ()
42−serveAppAt loc app cfg = do
43− route $ [ (loc, ifTop $ serveStaticIndex cfg)
44− , (loc, serveAssets (app </> "static.assets") (app </> "static"))
45− , (loc <> "/version", doNotCache >> serveFileIfExistsAs "text/plain" (app </> "version"))
46− ]
47− ++ if _appConfig_serveJsexe cfg
48− then [(loc, serveAssets (app </> frontendJsAssetsPath cfg) (app </> frontendJsPath cfg))]
49− else []
50− ++ [ (loc, doNotCache >> error404) ]
51−
52−-- Helper function used in 'serveAppAt', writes a 404 error message in ByteString
53−error404 :: MonadSnap m => m ()
54−error404 = do
55− modifyResponse $ setResponseCode 404
56− writeBS "404 Not Found"
57−
58−-- Partially applied 'serveAppAt', specialized to the root directory
59−serveApp :: MonadSnap m => FilePath -> AppConfig m -> m ()
60−serveApp = serveAppAt ""
61−
62−-- Helper function used in 'serveAppAt', returns a frontendJs/... filepath
63−frontendJsPath :: AppConfig m -> FilePath
64−frontendJsPath (AppConfig { _appConfig_jsexe = jsexe }) = "frontendJs" </> jsexe
65−
66−-- Helper function used in 'serveAppAt', returns a frontendJs.assets/... filepath
67−frontendJsAssetsPath :: AppConfig m -> FilePath
68−frontendJsAssetsPath (AppConfig { _appConfig_jsexe = jsexe }) = "frontendJs.assets" </> jsexe
69−
70−-- Helper funtion used in 'serveAppAt', writes a lazy bytestring to the body of Http response
71−serveStaticIndex :: MonadSnap m => AppConfig m -> m ()
72−serveStaticIndex cfg = do
73− -- Decode, pack, and append asset target to file path
74− appJsPath <- liftIO $ getAssetPath (frontendJsAssetsPath cfg) "/all.js"
75− initialBody <- _appConfig_initialBody cfg
76− -- Render and write html head and body to response
77− writeLBS $ renderBS $ doctypehtml_ $ do
78− head_ $ do
79− meta_ [charset_ "utf-8"] -- meta-data charset description
80− meta_ [name_ "viewport", content_ "width=device-width, initial-scale=1.0"] -- meta-data viewport description
81− toHtmlRaw $ _appConfig_initialHead cfg
82− body_ $ do -- <body>
83− toHtmlRaw initialBody -- HtmlT monad wrapper
84− script_ [type_ "application/javascript", src_ (maybe "/all.js" T.pack appJsPath), defer_ "defer"] ("" :: String)
85−
86−{- | Takes an AppConfig monad and uses it to generate & write Lazy ByteString
87− to the body of the https response. This one comes with hard coded values
88− for the logo -}
89−serveIndex :: MonadSnap m => AppConfig m -> m ()
90−serveIndex cfg = do
91− appJsPath <- liftIO $ getAssetPath (frontendJsAssetsPath cfg) "all.js"
92− writeLBS $ renderBS $ doctypehtml_ $ do
93− head_ $ do
94− meta_ [charset_ "utf-8"]
95− meta_ [name_ "viewport", content_ "width=device-width, initial-scale=1.0"]
96− body_ $ do
97− script_ [type_ "application/javascript", src_ (maybe "/all.js" T.pack appJsPath), defer_ "defer"] ("" :: String)
98−
99−makeLenses ''AppConfig
release.nix−1
@@ -23,7 +23,6 @@ let
2323 "obelisk-run"
2424 "obelisk-route"
2525 "obelisk-selftest"
26− "obelisk-snap"
2726 "obelisk-snap-extras"
2827 ];
2928