@@ -3,23 +3,34 @@
33{-# LANGUAGE OverloadedStrings #-}
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE ViewPatterns #-}
6+{-# LANGUAGE DeriveGeneric #-}
7+{-# LANGUAGE TypeApplications #-}
68module Obelisk.Command.Deploy where
79
810import Control.Lens
911import Control.Monad
10−import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try)
12+import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try, bracket_)
1113import Control.Monad.IO.Class (liftIO)
14+import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)
1215import Data.Bits
16+import qualified Data.ByteString.Lazy as BSL
1317import Data.Default
18+import qualified Data.HashMap.Strict as HM
1419import qualified Data.Map as Map
20+import Data.Text (Text)
1521import qualified Data.Set as Set
1622import qualified Data.Text as T
1723import qualified Data.Text.IO as T
24+import GHC.Generics
25+import Nix.Convert
26+import Nix.Pretty
27+import Nix.Value
1828import System.Directory
1929import System.Environment (getEnvironment)
2030import System.FilePath
31+import System.IO
2132import System.Posix.Files
22−import System.Process (delegate_ctlc, env, proc)
33+import System.Process (delegate_ctlc, env, proc, cwd)
2334import Text.URI (URI)
2435import qualified Text.URI as URI
2536import Text.URI.Lens
@@ -150,13 +161,106 @@ deployPush deployPath getNixBuilders = do
150161deployUpdate :: MonadObelisk m => FilePath -> m ()
151162deployUpdate deployPath = updateThunkToLatest $ deployPath </> "src"
152163
153−deployMobile :: MonadObelisk m => String -> [String] -> m ()
164+keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValueNF Identity)
165+keytoolToAndroidConfig conf = runIdentity $ do
166+ path <- toValue $ Path $ _keytoolConfig_keystore conf
167+ storepass <- toValue $ T.pack $ _keytoolConfig_storepass conf
168+ alias <- toValue $ T.pack $ _keytoolConfig_alias conf
169+ keypass <- toValue $ T.pack $ _keytoolConfig_keypass conf
170+ return $ HM.fromList
171+ [ ("storeFile", path)
172+ , ("storePassword", storepass)
173+ , ("keyAlias", alias)
174+ , ("keyPassword", keypass)
175+ ]
176+
177+data PlatformDeployment = Android | IOS
178+ deriving (Show, Eq)
179+
180+renderPlatformDeployment :: PlatformDeployment -> String
181+renderPlatformDeployment = \case
182+ Android -> "android"
183+ IOS -> "ios"
184+
185+deployMobile :: MonadObelisk m => PlatformDeployment -> [String] -> m ()
154186deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
155187 let srcDir = root </> "src"
156188 exists <- liftIO $ doesDirectoryExist srcDir
157189 unless exists $ failWith "ob test should be run inside of a deploy directory"
158− result <- nixBuildAttrWithCache srcDir $ platform <> ".frontend"
190+ nixBuildTarget <- case platform of
191+ Android -> do
192+ let keystorePath = root </> "android_keystore.jks"
193+ keytoolConfPath = root </> "android_keytool_config.json"
194+ hasKeystore <- liftIO $ doesFileExist keystorePath
195+ when (not hasKeystore) $ do
196+ -- TODO log instructions for how to modify the keystore
197+ putLog Notice $ "Creating keystore: " <> T.pack keystorePath
198+ putLog Notice "Enter a keystore password: "
199+ keyStorePassword <- liftIO $ withEcho False getLine
200+ putLog Notice "Re-enter the keystore password: "
201+ keyStorePassword' <- liftIO $ withEcho False getLine
202+ unless (keyStorePassword' == keyStorePassword) $ failWith "passwords do not match"
203+ let keyToolConf = KeytoolConfig
204+ { _keytoolConfig_keystore = keystorePath
205+ , _keytoolConfig_alias = "obelisk"
206+ , _keytoolConfig_storepass = keyStorePassword
207+ , _keytoolConfig_keypass = keyStorePassword
208+ }
209+ createKeystore root $ keyToolConf
210+ liftIO $ BSL.writeFile keytoolConfPath $ encode keyToolConf
211+ checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
212+ unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
213+ keytoolConfContents <- liftIO $ BSL.readFile keytoolConfPath
214+ liftIO $ putStrLn $ show keytoolConfContents
215+ releaseKey <- case eitherDecode keytoolConfContents of
216+ Left err -> failWith $ T.pack err
217+ Right conf -> do
218+ let nvset = toValue @(HM.HashMap Text (NValueNF Identity)) @Identity @(NValueNF Identity) $ keytoolToAndroidConfig conf
219+ return $ printNix $ runIdentity nvset
220+ return $ Target
221+ { _target_path = Nothing
222+ , _target_attr = Nothing
223+ , _target_expr = Just $ "with (import " <> srcDir <> " {}); " <> renderPlatformDeployment platform <> ".frontend.override (drv: { releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; })"
224+ }
225+ IOS -> return $ Target
226+ { _target_path = Nothing
227+ , _target_attr = Just "ios.frontend"
228+ , _target_expr = Nothing
229+ }
230+ result <- nixCmd $ NixCmd_Build $ def
231+ & nixBuildConfig_outLink .~ OutLink_None
232+ & nixCmdConfig_target .~ nixBuildTarget
233+ putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> (show result)
159234 callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
235+ where
236+ withEcho showEcho f = do
237+ prevEcho <- hGetEcho stdin
238+ bracket_ (hSetEcho stdin showEcho) (hSetEcho stdin prevEcho) f
239+
240+data KeytoolConfig = KeytoolConfig
241+ { _keytoolConfig_keystore :: FilePath
242+ , _keytoolConfig_alias :: String
243+ , _keytoolConfig_storepass :: String
244+ , _keytoolConfig_keypass :: String
245+ } deriving (Show, Generic)
246+
247+instance FromJSON KeytoolConfig
248+instance ToJSON KeytoolConfig
249+
250+createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
251+createKeystore root config = do
252+ let expr = "with (import " <> toImplDir root <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
253+ callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
254+ where
255+ keytoolCmd = processToShellString "keytool"
256+ [ "-genkeypair", "-noprompt"
257+ , "-keystore", _keytoolConfig_keystore config
258+ , "-keyalg", "RSA", "-keysize", "2048"
259+ , "-validity", "1000000000"
260+ , "-storepass", _keytoolConfig_storepass config
261+ , "-alias", _keytoolConfig_alias config
262+ , "-keypass", _keytoolConfig_keypass config
263+ ]
160264
161265-- | Simplified deployment configuration mechanism. At one point we may revisit this.
162266writeDeployConfig :: MonadObelisk m => FilePath -> FilePath -> String -> m ()