@@ -1,10 +1,20 @@
11{-# LANGUAGE LambdaCase #-}
22{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE StaticPointers #-}
4+{-# LANGUAGE TemplateHaskell #-}
5+{-# LANGUAGE TypeApplications #-}
36module Obelisk.Command where
47
8+import System.Process ()
59import Control.Monad
10+import qualified Data.Binary as Binary
11+import qualified Data.ByteString.Base16 as Base16
12+import qualified Data.ByteString.Lazy as LBS
613import Data.List
714import Data.Monoid
15+import qualified Data.Text as T
16+import Data.Text.Encoding
17+import GHC.StaticPtr
818import Options.Applicative
919import System.Environment
1020import System.FilePath
@@ -12,8 +22,11 @@ import System.IO
1222import System.Posix.Process
1323
1424import Obelisk.Command.Project
25+import Obelisk.Command.Deploy
1526import Obelisk.Command.Thunk
1627import Obelisk.Command.Repl
28+import Obelisk.Command.Run
29+import Obelisk.Command.Utils
1730
1831data Args = Args
1932 { _args_noHandOffPassed :: Bool
@@ -48,18 +61,69 @@ initSource = foldl1 (<|>)
4861
4962data ObCommand
5063 = ObCommand_Init InitSource
51− | ObCommand_Dev
64+ | ObCommand_Deploy DeployCommand
65+ | ObCommand_Run
5266 | ObCommand_Thunk ThunkCommand
5367 | ObCommand_Repl FilePath
5468 | ObCommand_Watch FilePath
69+ | ObCommand_Internal ObInternal
70+
71+data ObInternal
72+ = ObInternal_RunStaticIO StaticKey
73+
74+inNixShell' :: StaticPtr (IO ()) -> IO ()
75+inNixShell' p = withProjectRoot "." $ \root -> do
76+ progName <- getExecutablePath
77+ projectShell root False "ghc" $ unwords --TODO: shell escape
78+ [ progName
79+ , "internal"
80+ , "run-static-io"
81+ , encodeStaticKey $ staticKey p
82+ ]
5583
5684obCommand :: Parser ObCommand
57−obCommand = hsubparser $ mconcat
58− [ command "init" $ info (ObCommand_Init <$> initSource) $ progDesc "Initialize an Obelisk project"
59− , command "dev" $ info (pure ObCommand_Dev) $ progDesc "Run current project in development mode"
60− , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
61− , command "repl" $ info (ObCommand_Repl <$> (strArgument (action "directory"))) $ progDesc "Open an interactive interpreter"
62− , command "watch" $ info (ObCommand_Watch <$> (strArgument (action "directory")))$ progDesc "Watch directory for changes and update interactive interpreter"
85+obCommand = hsubparser
86+ (mconcat
87+ [ command "init" $ info (ObCommand_Init <$> initSource) $ progDesc "Initialize an Obelisk project"
88+ , command "deploy" $ info (ObCommand_Deploy <$> deployCommand) $ progDesc "Prepare a deployment for an Obelisk project"
89+ , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
90+ , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
91+ , command "repl" $ info (ObCommand_Repl <$> (strArgument (action "directory"))) $ progDesc "Open an interactive interpreter"
92+ , command "watch" $ info (ObCommand_Watch <$> (strArgument (action "directory")))$ progDesc "Watch directory for changes and update interactive interpreter"
93+ ])
94+ <|> subparser
95+ (mconcat
96+ [ internal
97+ , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
98+ ])
99+
100+deployCommand :: Parser DeployCommand
101+deployCommand = hsubparser $ mconcat
102+ [ command "init" $ info deployInitCommand $ progDesc "Initialize a deployment configuration directory"
103+ , command "push" $ info (pure DeployCommand_Push) mempty
104+ ]
105+
106+deployInitCommand :: Parser DeployCommand
107+deployInitCommand = fmap DeployCommand_Init $ DeployInitOpts
108+ <$> strArgument (action "deploy-dir" <> metavar "DEPLOYDIR" <> help "Path to a directory that it will create")
109+ <*> strOption (long "ssh-key" <> metavar "SSHKEY" <> help "Path to an ssh key that it will symlink to")
110+ <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
111+ <*> strOption (long "remote" <> metavar "REMOTE" <> help "git remote to use for the src thunk")
112+
113+data DeployCommand
114+ = DeployCommand_Init DeployInitOpts
115+ | DeployCommand_Push
116+
117+data DeployInitOpts = DeployInitOpts
118+ { _deployInitOpts_ouputDir :: FilePath
119+ , _deployInitOpts_sshKey :: FilePath
120+ , _deployInitOpts_hostname :: [String]
121+ , _deployInitOpts_remote :: String
122+ }
123+
124+internalCommand :: Parser ObInternal
125+internalCommand = subparser $ mconcat
126+ [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
63127 ]
64128
65129--TODO: Result should provide normalised path and also original user input for error reporting.
@@ -131,12 +195,43 @@ main = do
131195ob :: ObCommand -> IO ()
132196ob = \case
133197 ObCommand_Init source -> initProject source
134− ObCommand_Dev -> putStrLn "Dev!"
198+ ObCommand_Deploy dc -> case dc of
199+ DeployCommand_Init deployOpts -> withProjectRoot "." $ \root -> do
200+ thunkPtr <- readThunk root >>= \case
201+ Left err -> fail $ "thunk pack: " <> show err
202+ Right (ThunkData_Packed ptr) -> return ptr
203+ Right (ThunkData_Checkout (Just ptr)) -> return ptr
204+ Right (ThunkData_Checkout Nothing) ->
205+ getThunkPtr root (_deployInitOpts_remote deployOpts)
206+ let deployDir = _deployInitOpts_ouputDir deployOpts
207+ sshKeyPath = _deployInitOpts_sshKey deployOpts
208+ hostname = _deployInitOpts_hostname deployOpts
209+ deployInit thunkPtr (root </> "config") deployDir sshKeyPath hostname
210+ DeployCommand_Push -> do
211+ checkGitCleanStatus "." >>= \case
212+ True -> return ()
213+ False -> fail "ob push: Commit any changes to the deployment configuration before proceeding"
214+ ObCommand_Run -> inNixShell' $ static run
215+ -- inNixShell ($(mkClosure 'ghcidAction) ())
135216 ObCommand_Thunk tc -> case tc of
136217 ThunkCommand_Update thunks -> mapM_ updateThunkToLatest thunks
137218 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
138219 ThunkCommand_Pack thunks -> forM_ thunks $ \(ThunkPackOpts dir upstream) -> packThunk dir upstream
139220 ObCommand_Repl component -> runRepl component
140221 ObCommand_Watch component -> watch component
141−
222+ ObCommand_Internal icmd -> case icmd of
223+ ObInternal_RunStaticIO k -> unsafeLookupStaticPtr @(IO ()) k >>= \case
224+ Nothing -> fail $ "ObInternal_RunStaticIO: no such StaticKey: " <> show k
225+ Just p -> deRefStaticPtr p
142226--TODO: Clean up all the magic strings throughout this codebase
227+
228+encodeStaticKey :: StaticKey -> String
229+encodeStaticKey = T.unpack . decodeUtf8 . Base16.encode . LBS.toStrict . Binary.encode
230+
231+decodeStaticKey :: String -> Either String StaticKey
232+decodeStaticKey s = case Base16.decode $ encodeUtf8 $ T.pack s of
233+ (b, "") -> case Binary.decodeOrFail $ LBS.fromStrict b of
234+ Right ("", _, a) -> pure a
235+ Right _ -> fail $ "decodeStaticKey: Binary.decodeOrFail didn't consume all input"
236+ Left (_, _, e) -> fail $ "decodeStaticKey: Binary.decodeOrFail failed: " <> show e
237+ _ -> fail $ "decodeStaticKey: could not decode hex string: " <> show s