1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE MultiParamTypeClasses #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE StaticPointers #-}
7{-# LANGUAGE TypeApplications #-}
8module Obelisk.Command where
11import Control.Monad.IO.Class (liftIO)
12import Control.Monad.Reader (ask)
13import qualified Data.Binary as Binary
14import Data.Bool (bool)
15import qualified Data.ByteString.Base16 as Base16
16import qualified Data.ByteString.Lazy as LBS
18import Data.Maybe (catMaybes)
20import qualified Data.Text as T
21import Data.Text.Encoding
23import Options.Applicative
24import System.Directory
25import System.Environment
27import System.IO (hIsTerminalDevice, stdout)
28import System.Posix.Process (executeFile)
29import System.Process (callCommand)
32import Obelisk.CLI (Severity (..), cliDemo, failWith, getLogLevel, newLoggingConfig, putLog)
33import Obelisk.Command.Deploy
34import Obelisk.Command.Project
35import Obelisk.Command.Repl
36import Obelisk.Command.Run
37import Obelisk.Command.Thunk
40 { _args_noHandOffPassed :: Bool
41 -- ^ This flag is actually handled outside of the optparse-applicative parser, but we detect whether
42 -- it has gotten through in order to notify the user that it should only be passed once and as the very
44 , _args_verbose :: Bool
45 , _args_command :: ObCommand
50args = Args <$> noHandoff <*> verbose <*> obCommand
52noHandoff :: Parser Bool
53noHandoff = flag False True $ mconcat
55 , help "Do not hand off execution to project-specific implementation of this command"
60verbose = flag False True $ mconcat
63 , help "Be more verbose"
66argsInfo :: ParserInfo Args
67argsInfo = info (args <**> helper) $ mconcat
69 , progDesc "Manage Obelisk projects"
72initSource :: Parser InitSource
73initSource = foldl1 (<|>)
74 [ pure InitSource_Default
75 , InitSource_Branch <$> strOption (long "branch" <> metavar "BRANCH")
76 , InitSource_Symlink <$> strOption (long "symlink" <> action "directory" <> metavar "PATH")
80 = ObCommand_Init InitSource
81 | ObCommand_Deploy DeployCommand
83 | ObCommand_Thunk ThunkCommand
84 | ObCommand_Repl FilePath
85 | ObCommand_Watch FilePath
86 | ObCommand_Internal ObInternal
90 = ObInternal_RunStaticIO StaticKey
94inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
95inNixShell' p = withProjectRoot "." $ \root -> do
96 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
97 projectShell root False "ghc" cmd
101 progName <- getExecutablePath
102 return $ progName : catMaybes
103 [ Just "--no-handoff"
104 , bool Nothing (Just "--verbose") $ _args_verbose obArgs
106 , Just "run-static-io"
107 , Just $ encodeStaticKey $ staticKey p
110obCommand :: Parser ObCommand
111obCommand = hsubparser
113 [ command "init" $ info (ObCommand_Init <$> initSource) $ progDesc "Initialize an Obelisk project"
114 , command "deploy" $ info (ObCommand_Deploy <$> deployCommand) $ progDesc "Prepare a deployment for an Obelisk project"
115 , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
116 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
117 , command "repl" $ info (ObCommand_Repl <$> strArgument (action "directory")) $ progDesc "Open an interactive interpreter"
118 , command "watch" $ info (ObCommand_Watch <$> strArgument (action "directory"))$ progDesc "Watch directory for changes and update interactive interpreter"
123 , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
126deployCommand :: Parser DeployCommand
127deployCommand = hsubparser $ mconcat
128 [ command "init" $ info deployInitCommand $ progDesc "Initialize a deployment configuration directory"
129 , command "push" $ info (pure DeployCommand_Push) mempty
130 , command "test" $ info (DeployCommand_Test <$> argument (maybeReader readPlatform) (completeWith ["android"])) $ progDesc "Test your obelisk project from a mobile platform. [Only android supported]"
131 , command "update" $ info (pure DeployCommand_Update) $ progDesc "Update the deployment's src thunk to latest"
134readPlatform :: String -> Maybe Platform
136 "android" -> Just Android
139deployInitCommand :: Parser DeployCommand
140deployInitCommand = fmap DeployCommand_Init $ DeployInitOpts
141 <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory that it will create")
142 <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an ssh key that it will symlink to")
143 <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
144 <*> strOption (long "upstream" <> value "origin" <> metavar "REMOTE" <> help "git remote to use for the src thunk" <> showDefault)
146data Platform = Android
150 = DeployCommand_Init DeployInitOpts
152 | DeployCommand_Test Platform
153 | DeployCommand_Update
156data DeployInitOpts = DeployInitOpts
157 { _deployInitOpts_outputDir :: FilePath
158 , _deployInitOpts_sshKey :: FilePath
159 , _deployInitOpts_hostname :: [String]
160 , _deployInitOpts_remote :: String
164internalCommand :: Parser ObInternal
165internalCommand = subparser $ mconcat
166 [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
167 , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
170--TODO: Result should provide normalised path and also original user input for error reporting.
171thunkDirectoryParser :: Parser FilePath
172thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgument $ mconcat
175 , help "Path to directory containing thunk data"
178data ThunkPackOpts = ThunkPackOpts
179 { _thunkPackOpts_directory :: FilePath
180 , _thunkPackOpts_upstream :: String
185 = ThunkCommand_Update [FilePath]
186 | ThunkCommand_Unpack [FilePath]
187 | ThunkCommand_Pack [ThunkPackOpts]
190thunkPackOpts :: Parser ThunkPackOpts
191thunkPackOpts = (ThunkPackOpts <$> thunkDirectoryParser <*>) . strOption $ mconcat
195 , help "Git remote that packed thunk will point to"
199thunkCommand :: Parser ThunkCommand
200thunkCommand = hsubparser $ mconcat
201 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser) $ progDesc "Update thunk to latest revision available"
202 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
203 , command "pack" $ info (ThunkCommand_Pack <$> some thunkPackOpts) $ progDesc "Pack git checkout into thunk that points at given upstream"
206parserPrefs :: ParserPrefs
207parserPrefs = defaultPrefs
208 { prefShowHelpOnEmpty = True
211-- | Are we on a terminal with active user interaction?
212isInteractiveTerm :: IO Bool
213isInteractiveTerm = do
214 isTerm <- hIsTerminalDevice stdout
215 -- Running in bash/fish/zsh completion
216 inShellCompletion <- liftIO $ isInfixOf "completion" . unwords <$> getArgs
217 return $ isTerm && not inShellCompletion
219mkObeliskConfig :: IO Obelisk
221 notInteractive <- not <$> isInteractiveTerm
222 logLevel <- toLogLevel <$> getObArgs
223 loggingConfig <- newLoggingConfig logLevel notInteractive
224 return $ Obelisk notInteractive loggingConfig
226 toLogLevel = bool Notice Debug . _args_verbose
229getObArgs = getArgs >>= handleParseResult . execParserPure parserPrefs argsInfo
232main = mkObeliskConfig >>= (`runObelisk` ObeliskT main')
234main' :: MonadObelisk m => m ()
237 obPath <- liftIO getExecutablePath
238 obArgs <- liftIO getObArgs
239 logLevel <- liftIO $ getLogLevel $ _obelisk_logging c
240 putLog Debug $ T.pack $ unwords
241 [ "Starting Obelisk <" <> obPath <> ">"
242 , "args=" <> show obArgs
243 , "noSpinner=" <> show (_obelisk_noSpinner c)
244 , "logging-level=" <> show logLevel
247 --TODO: We'd like to actually use the parser to determine whether to hand off,
248 --but in the case where this implementation of 'ob' doesn't support all
249 --arguments being passed along, this could fail. For now, we don't bother
250 --with optparse-applicative until we've done the handoff.
251 myArgs <- liftIO getArgs
253 args' <- liftIO $ handleParseResult (execParserPure parserPrefs argsInfo as)
254 case _args_noHandOffPassed args' of
256 True -> putLog Warning "--no-handoff should only be passed once and as the first argument; ignoring"
257 ob $ _args_command args'
258 handoffAndGo as = findProjectObeliskCommand "." >>= \case
259 Nothing -> go as -- If not in a project, just run ourselves
261 -- Invoke the real implementation, using --no-handoff to prevent infinite recursion
262 putLog Debug $ "Handing off to " <> T.pack impl
263 liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
265 "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
266 a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
267 | "--bash-completion" `isPrefixOf` a
268 && "--no-handoff" `elem` as -> go (a:as)
269 | otherwise -> handoffAndGo (a:as)
270 as -> handoffAndGo as
272ob :: MonadObelisk m => ObCommand -> m ()
274 ObCommand_Init source -> initProject source
275 ObCommand_Deploy dc -> case dc of
276 DeployCommand_Init deployOpts -> withProjectRoot "." $ \root -> do
277 let deployDir = _deployInitOpts_outputDir deployOpts
278 r <- liftIO $ canonicalizePath root
279 rootEqualsTarget <- liftIO $ equalFilePath r <$> canonicalizePath deployDir
280 when rootEqualsTarget $
281 failWith $ "Deploy directory " <> T.pack deployDir <> " should not be the same as project root."
282 thunkPtr <- readThunk root >>= \case
283 Left err -> failWith $ case err of
284 ReadThunkError_AmbiguousFiles ->
285 "Project root " <> T.pack r <> " is not a git repository or valid thunk"
286 ReadThunkError_UnrecognizedFiles ->
287 "Project root " <> T.pack r <> " is not a git repository or valid thunk"
288 _ -> "thunk read: " <> T.pack (show err)
289 Right (ThunkData_Packed ptr) -> return ptr
290 Right (ThunkData_Checkout (Just ptr)) -> return ptr
291 Right (ThunkData_Checkout Nothing) ->
292 getThunkPtr' False root (T.pack $ _deployInitOpts_remote deployOpts)
293 let sshKeyPath = _deployInitOpts_sshKey deployOpts
294 hostname = _deployInitOpts_hostname deployOpts
295 deployInit thunkPtr (root </> "config") deployDir sshKeyPath hostname
296 DeployCommand_Push -> deployPush "."
297 DeployCommand_Test Android -> withProjectRoot "." $ \root -> do
298 let srcDir = root </> "src"
299 exists <- liftIO $ doesDirectoryExist srcDir
300 unless exists $ failWith "ob test should be run inside of a deploy directory"
301 result <- nixBuildAttrWithCache srcDir "android.frontend"
302 liftIO $ callCommand $ result </> "bin" </> "deploy"
303 DeployCommand_Update -> deployUpdate "."
304 ObCommand_Run -> inNixShell' $ static run
305 -- inNixShell ($(mkClosure 'ghcidAction) ())
306 ObCommand_Thunk tc -> case tc of
307 ThunkCommand_Update thunks -> mapM_ updateThunkToLatest thunks
308 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
309 ThunkCommand_Pack thunks -> forM_ thunks $ \(ThunkPackOpts dir upstream) -> packThunk dir (T.pack upstream)
310 ObCommand_Repl component -> runRepl component
311 ObCommand_Watch component -> watch component
312 ObCommand_Internal icmd -> case icmd of
313 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
314 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
317 liftIO $ runObelisk c $ deRefStaticPtr p
318 ObInternal_CLIDemo -> cliDemo
320--TODO: Clean up all the magic strings throughout this codebase
322encodeStaticKey :: StaticKey -> String
323encodeStaticKey = T.unpack . decodeUtf8 . Base16.encode . LBS.toStrict . Binary.encode
325-- TODO: Use failWith in place of fail to be consistent.
326decodeStaticKey :: String -> Either String StaticKey
327decodeStaticKey s = case Base16.decode $ encodeUtf8 $ T.pack s of
328 (b, "") -> case Binary.decodeOrFail $ LBS.fromStrict b of
329 Right ("", _, a) -> pure a
330 Right _ -> fail "decodeStaticKey: Binary.decodeOrFail didn't consume all input"
331 Left (_, _, e) -> fail $ "decodeStaticKey: Binary.decodeOrFail failed: " <> show e
332 _ -> fail $ "decodeStaticKey: could not decode hex string: " <> show s
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list