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 qualified Data.Binary as Binary
13import Data.Bool (bool)
14import qualified Data.ByteString.Base16 as Base16
15import qualified Data.ByteString.Lazy as LBS
17import Data.Maybe (catMaybes)
18import qualified Data.Text as T
19import Data.Text.Encoding
20import Data.Text.Encoding.Error (lenientDecode)
22import Options.Applicative
23import System.Directory
24import System.Environment
26import qualified System.Info
27import System.IO (hIsTerminalDevice, stdout)
28import System.Posix.Process (executeFile)
32import Obelisk.CliApp.Demo (cliDemo)
33import Obelisk.Command.Deploy
34import Obelisk.Command.Project
35import Obelisk.Command.Run
36import Obelisk.Command.Thunk
37import Obelisk.Command.Utils
38import qualified Obelisk.Command.VmBuilder as VmBuilder
42 { _args_noHandOffPassed :: Bool
43 -- ^ This flag is actually handled outside of the optparse-applicative parser, but we detect whether
44 -- it has gotten through in order to notify the user that it should only be passed once and as the very
46 , _args_verbose :: Bool
47 , _args_command :: ObCommand
51newtype ArgsConfig = ArgsConfig
52 { _argsConfig_enableVmBuilderByDefault :: Bool
55args :: ArgsConfig -> Parser Args
56args cfg = Args <$> noHandoff <*> verbose <*> obCommand cfg
58noHandoff :: Parser Bool
59noHandoff = flag False True $ mconcat
61 , help "Do not hand off execution to project-specific implementation of this command"
66verbose = flag False True $ mconcat
69 , help "Be more verbose"
72argsInfo :: ArgsConfig -> ParserInfo Args
73argsInfo cfg = info (args cfg <**> helper) $ mconcat
75 , progDesc "Manage Obelisk projects"
78initSource :: Parser InitSource
79initSource = foldl1 (<|>)
80 [ pure InitSource_Default
81 , InitSource_Branch <$> strOption (long "branch" <> metavar "BRANCH")
82 , InitSource_Symlink <$> strOption (long "symlink" <> action "directory" <> metavar "PATH")
85initForce :: Parser Bool
86initForce = switch (long "force" <> help "Allow ob init to overwrite files")
89 = ObCommand_Init InitSource Bool
90 | ObCommand_Deploy DeployCommand
92 | ObCommand_Thunk ThunkCommand
95 | ObCommand_Shell ShellOpts
96 | ObCommand_Internal ObInternal
100 = ObInternal_RunStaticIO StaticKey
104inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
105inNixShell' p = withProjectRoot "." $ \root -> do
106 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
107 projectShell root False "ghc" (Just cmd)
110 argsCfg <- getArgsConfig
112 obArgs <- parseCLIArgs argsCfg myArgs
113 progName <- getObeliskExe
114 return $ progName : catMaybes
115 [ Just "--no-handoff"
116 , bool Nothing (Just "--verbose") $ _args_verbose obArgs
118 , Just "run-static-io"
119 , Just $ encodeStaticKey $ staticKey p
122obCommand :: ArgsConfig -> Parser ObCommand
123obCommand cfg = hsubparser
125 [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
126 , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
127 , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
128 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
129 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
130 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
131 , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
136 , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
139deployCommand :: ArgsConfig -> Parser DeployCommand
140deployCommand cfg = hsubparser $ mconcat
141 [ command "init" $ info (DeployCommand_Init <$> deployInitOpts) $ progDesc "Initialize a deployment configuration directory"
142 , command "push" $ info (DeployCommand_Push <$> remoteBuilderParser) mempty
143 , command "test" $ info (DeployCommand_Test <$> platformP) $ progDesc "Test your obelisk project from a mobile platform."
144 , command "update" $ info (pure DeployCommand_Update) $ progDesc "Update the deployment's src thunk to latest"
147 platformP = hsubparser $ mconcat
148 [ command "android" $ info (pure (Android, [])) mempty
149 , command "ios" $ info ((,) <$> pure IOS <*> (fmap pure $ strArgument (metavar "TEAMID" <> help "Your Team ID - found in the Apple developer portal"))) mempty
152 remoteBuilderParser :: Parser (Maybe RemoteBuilder)
153 remoteBuilderParser =
154 flag (if enabledByDefault then enabled else Nothing) enabled (mconcat
155 [ long $ "enable-" <> flagBase
156 , help $ "Enable " <> flagDesc <> (if enabledByDefault then " (default)" else "")
158 <|> flag enabled Nothing (mconcat
159 [ long $ "disable-" <> flagBase
160 , help $ "Disable a " <> flagDesc <> (if not enabledByDefault then " (default)" else "")
163 enabledByDefault = _argsConfig_enableVmBuilderByDefault cfg
164 enabled = Just RemoteBuilder_ObeliskVM
165 flagBase = "vm-builder"
166 flagDesc = "managed Linux virtual machine as a Nix remote builder (requires Docker)"
169deployInitOpts :: Parser DeployInitOpts
170deployInitOpts = DeployInitOpts
171 <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory that it will create")
172 <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an ssh key that it will symlink to")
173 <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
174 <*> strOption (long "route" <> metavar "PUBLICROUTE" <> help "Publicly accessible URL of your app")
175 <*> strOption (long "admin-email" <> metavar "ADMINEMAIL" <> help "Email address where administrative alerts will be sent")
176 <*> flag True False (long "disable-https" <> help "Disable automatic https configuration for the backend")
179data RemoteBuilder = RemoteBuilder_ObeliskVM
183 = DeployCommand_Init DeployInitOpts
184 | DeployCommand_Push (Maybe RemoteBuilder)
185 | DeployCommand_Test (PlatformDeployment, [String])
186 | DeployCommand_Update
189data DeployInitOpts = DeployInitOpts
190 { _deployInitOpts_outputDir :: FilePath
191 , _deployInitOpts_sshKey :: FilePath
192 , _deployInitOpts_hostname :: [String]
193 , _deployInitOpts_route :: String
194 , _deployInitOpts_adminEmail :: String
195 , _deployInitOpts_enableHttps :: Bool
199internalCommand :: Parser ObInternal
200internalCommand = subparser $ mconcat
201 [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
202 , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
205--TODO: Result should provide normalised path and also original user input for error reporting.
206thunkDirectoryParser :: Parser FilePath
207thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgument $ mconcat
210 , help "Path to directory containing thunk data"
214 = ThunkCommand_Update [FilePath] (Maybe String)
215 | ThunkCommand_Unpack [FilePath]
216 | ThunkCommand_Pack [FilePath] Bool
219forceFlag :: Parser Bool
220forceFlag = switch $ long "force" <> short 'f' <> help "Force packing thunks even if there are branches not pushed upstream, uncommitted changes, stashes. This will cause changes that have not been pushed upstream to be lost; use with care."
222thunkCommand :: Parser ThunkCommand
223thunkCommand = hsubparser $ mconcat
224 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
225 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
226 , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> forceFlag) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
231 { _shellOpts_shell :: String
232 , _shellOpts_command :: Maybe String
236shellOpts :: Parser ShellOpts
238 <$> ( flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
239 <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
240 <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
242 <*> optional (strArgument (metavar "COMMAND"))
244parserPrefs :: ParserPrefs
245parserPrefs = defaultPrefs
246 { prefShowHelpOnEmpty = True
249parseCLIArgs :: ArgsConfig -> [String] -> IO Args
250parseCLIArgs cfg as = pure as >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
252-- | Create an Obelisk config for the current process.
253mkObeliskConfig :: IO Obelisk
256 -- This function should not use argument parser (full argument parsing happens post handoff)
257 let logLevel = toLogLevel $ "-v" `elem` cliArgs
258 notInteractive <- not <$> isInteractiveTerm
259 cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
260 ObeliskError_ProcessError (ProcessFailure p code) ann ->
261 ( "Process exited with code " <> T.pack (show code) <> "; " <> reconstructCommand p
262 <> maybe "" ("\n" <>) ann
265 ObeliskError_Unstructured msg -> (msg, 2)
267 return $ Obelisk cliConf
269 toLogLevel = bool Notice Debug
270 isInteractiveTerm = do
271 isTerm <- hIsTerminalDevice stdout
272 -- Running in bash/fish/zsh completion
273 inShellCompletion <- liftIO $ isInfixOf "completion" . unwords <$> getArgs
275 -- Respect the user’s TERM environment variable. Dumb terminals
276 -- like Eshell cannot handle lots of control sequences that the
278 termEnv <- lookupEnv "TERM"
279 let isDumb = termEnv == Just "dumb"
281 return $ isTerm && not inShellCompletion && not isDumb
283-- | For use from development obelisk repls
286-- > runCommand $ someFuncInMonadObelisk ...
287runCommand :: ObeliskT IO a -> IO a
288runCommand f = flip runObelisk f =<< mkObeliskConfig
291main = runCommand . main' =<< getArgsConfig
293main' :: MonadObelisk m => ArgsConfig -> m ()
295 obPath <- liftIO getExecutablePath
296 myArgs <- liftIO getArgs
297 logLevel <- getLogLevel
298 putLog Debug $ T.pack $ unwords
299 [ "Starting Obelisk <" <> obPath <> ">"
300 , "args=" <> show myArgs
301 , "logging-level=" <> show logLevel
304 --TODO: We'd like to actually use the parser to determine whether to hand off,
305 --but in the case where this implementation of 'ob' doesn't support all
306 --arguments being passed along, this could fail. For now, we don't bother
307 --with optparse-applicative until we've done the handoff.
309 args' <- liftIO $ handleParseResult (execParserPure parserPrefs (argsInfo argsCfg) as)
310 case _args_noHandOffPassed args' of
312 True -> putLog Warning "--no-handoff should only be passed once and as the first argument; ignoring"
313 ob $ _args_command args'
314 handoffAndGo as = findProjectObeliskCommand "." >>= \case
315 Nothing -> go as -- If not in a project, just run ourselves
317 -- Invoke the real implementation, using --no-handoff to prevent infinite recursion
318 putLog Debug $ "Handing off to " <> T.pack impl
319 liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
321 "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
322 a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
323 | "--bash-completion" `isPrefixOf` a
324 && "--no-handoff" `elem` as -> go (a:as)
325 | otherwise -> handoffAndGo (a:as)
326 as -> handoffAndGo as
328ob :: MonadObelisk m => ObCommand -> m ()
330 ObCommand_Init source force -> initProject source force
331 ObCommand_Deploy dc -> case dc of
332 DeployCommand_Init deployOpts -> withProjectRoot "." $ \root -> do
333 let deployDir = _deployInitOpts_outputDir deployOpts
334 r <- liftIO $ canonicalizePath root
335 rootEqualsTarget <- liftIO $ equalFilePath r <$> canonicalizePath deployDir
336 when rootEqualsTarget $
337 failWith $ "Deploy directory " <> T.pack deployDir <> " should not be the same as project root."
338 thunkPtr <- readThunk root >>= \case
339 Left err -> failWith $ case err of
340 ReadThunkError_AmbiguousFiles ->
341 "Project root " <> T.pack r <> " is not a git repository or valid thunk"
342 ReadThunkError_UnrecognizedFiles ->
343 "Project root " <> T.pack r <> " is not a git repository or valid thunk"
344 _ -> "thunk read: " <> T.pack (show err)
345 Right (ThunkData_Packed ptr) -> return ptr
346 Right (ThunkData_Checkout (Just ptr)) -> return ptr
347 Right (ThunkData_Checkout Nothing) ->
348 getThunkPtr False root
349 let sshKeyPath = _deployInitOpts_sshKey deployOpts
350 hostname = _deployInitOpts_hostname deployOpts
351 route = _deployInitOpts_route deployOpts
352 adminEmail = _deployInitOpts_adminEmail deployOpts
353 enableHttps = _deployInitOpts_enableHttps deployOpts
354 deployInit thunkPtr deployDir sshKeyPath hostname route adminEmail enableHttps
355 DeployCommand_Push remoteBuilder -> do
356 deployPath <- liftIO $ canonicalizePath "."
357 deployPush deployPath $ case remoteBuilder of
359 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
360 DeployCommand_Update -> deployUpdate "."
361 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
362 ObCommand_Run -> inNixShell' $ static run
363 -- inNixShell ($(mkClosure 'ghcidAction) ())
364 ObCommand_Thunk tc -> case tc of
365 ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
366 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
367 ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
368 ObCommand_Repl -> runRepl
369 ObCommand_Watch -> inNixShell' $ static runWatch
370 ObCommand_Shell so -> withProjectRoot "." $ \root ->
371 projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
372 ObCommand_Internal icmd -> case icmd of
373 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
374 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
377 liftIO $ runObelisk c $ deRefStaticPtr p
378 ObInternal_CLIDemo -> cliDemo
380--TODO: Clean up all the magic strings throughout this codebase
382getArgsConfig :: IO ArgsConfig
383getArgsConfig = pure $ ArgsConfig { _argsConfig_enableVmBuilderByDefault = System.Info.os == "darwin" }
385encodeStaticKey :: StaticKey -> String
386encodeStaticKey = T.unpack . decodeUtf8With lenientDecode . Base16.encode . LBS.toStrict . Binary.encode
388-- TODO: Use failWith in place of fail to be consistent.
389decodeStaticKey :: String -> Either String StaticKey
390decodeStaticKey s = case Base16.decode $ encodeUtf8 $ T.pack s of
391 (b, "") -> case Binary.decodeOrFail $ LBS.fromStrict b of
392 Right ("", _, a) -> pure a
393 Right _ -> fail "decodeStaticKey: Binary.decodeOrFail didn't consume all input"
394 Left (_, _, e) -> fail $ "decodeStaticKey: Binary.decodeOrFail failed: " <> show e
395 _ -> 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