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
16import Data.Foldable (for_)
18import qualified Data.Text as T
19import Data.Text.Encoding
20import Data.Text.Encoding.Error (lenientDecode)
22import Options.Applicative
23import Options.Applicative.Help.Pretty (text, (<$$>))
24import System.Directory
25import System.Environment
27import qualified System.Info
28import System.IO (hIsTerminalDevice, stdout)
29import System.Posix.Process (executeFile)
33import Obelisk.CliApp.Demo (cliDemo)
34import Obelisk.Command.Deploy
35import Obelisk.Command.Project
36import Obelisk.Command.Run
37import Obelisk.Command.Thunk
38import qualified Obelisk.Command.VmBuilder as VmBuilder
39import qualified Obelisk.Command.Preprocessor as Preprocessor
43 { _args_noHandOffPassed :: Bool
44 -- ^ This flag is actually handled outside of the optparse-applicative parser, but we detect whether
45 -- it has gotten through in order to notify the user that it should only be passed once and as the very
47 , _args_verbose :: Bool
48 , _args_command :: ObCommand
52newtype ArgsConfig = ArgsConfig
53 { _argsConfig_enableVmBuilderByDefault :: Bool
56args :: ArgsConfig -> Parser Args
57args cfg = Args <$> noHandoff <*> verbose <*> obCommand cfg
59noHandoff :: Parser Bool
60noHandoff = flag False True $ mconcat
62 , help "Do not hand off execution to project-specific implementation of this command"
67verbose = flag False True $ mconcat
70 , help "Be more verbose"
73argsInfo :: ArgsConfig -> ParserInfo Args
74argsInfo cfg = info (args cfg <**> helper) $ mconcat
76 , progDesc "Manage Obelisk projects"
79initSource :: Parser InitSource
80initSource = foldl1 (<|>)
81 [ pure InitSource_Default
82 , InitSource_Branch <$> strOption (long "branch" <> metavar "BRANCH")
83 , InitSource_Symlink <$> strOption (long "symlink" <> action "directory" <> metavar "PATH")
86initForce :: Parser Bool
87initForce = switch (long "force" <> help "Allow ob init to overwrite files")
90 = ObCommand_Init InitSource Bool
91 | ObCommand_Deploy DeployCommand
93 | ObCommand_Thunk ThunkCommand
96 | ObCommand_Shell ShellOpts
97 | ObCommand_Doc String [String] -- shell and list of packages
98 | ObCommand_Internal ObInternal
102 = ObInternal_RunStaticIO StaticKey
104 -- the preprocessor argument syntax is also handled outside
105 -- optparse-applicative, but it shouldn't ever conflict with another syntax
106 | ObInternal_ApplyPackages String String String [String]
109obCommand :: ArgsConfig -> Parser ObCommand
110obCommand cfg = hsubparser
112 [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
113 , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
114 , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
115 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
116 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
117 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
118 , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
119 , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
120 progDesc "List paths to haddock documentation for specified packages"
122 text "Hint: To open the documentation you can pipe the output of this command like"
123 <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
128 , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
131packageNames :: Parser [String]
132packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
134deployCommand :: ArgsConfig -> Parser DeployCommand
135deployCommand cfg = hsubparser $ mconcat
136 [ command "init" $ info (DeployCommand_Init <$> deployInitOpts) $ progDesc "Initialize a deployment configuration directory"
137 , command "push" $ info (DeployCommand_Push <$> remoteBuilderParser) mempty
138 , command "test" $ info (DeployCommand_Test <$> platformP) $ progDesc "Test your obelisk project from a mobile platform."
139 , command "update" $ info (pure DeployCommand_Update) $ progDesc "Update the deployment's src thunk to latest"
142 platformP = hsubparser $ mconcat
143 [ command "android" $ info (pure (Android, [])) mempty
144 , command "ios" $ info ((,) <$> pure IOS <*> (fmap pure $ strArgument (metavar "TEAMID" <> help "Your Team ID - found in the Apple developer portal"))) mempty
147 remoteBuilderParser :: Parser (Maybe RemoteBuilder)
148 remoteBuilderParser =
149 flag (if enabledByDefault then enabled else Nothing) enabled (mconcat
150 [ long $ "enable-" <> flagBase
151 , help $ "Enable " <> flagDesc <> (if enabledByDefault then " (default)" else "")
153 <|> flag enabled Nothing (mconcat
154 [ long $ "disable-" <> flagBase
155 , help $ "Disable a " <> flagDesc <> (if not enabledByDefault then " (default)" else "")
158 enabledByDefault = _argsConfig_enableVmBuilderByDefault cfg
159 enabled = Just RemoteBuilder_ObeliskVM
160 flagBase = "vm-builder"
161 flagDesc = "managed Linux virtual machine as a Nix remote builder (requires Docker)"
164deployInitOpts :: Parser DeployInitOpts
165deployInitOpts = DeployInitOpts
166 <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory that it will create")
167 <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an ssh key that it will symlink to")
168 <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
169 <*> strOption (long "route" <> metavar "PUBLICROUTE" <> help "Publicly accessible URL of your app")
170 <*> strOption (long "admin-email" <> metavar "ADMINEMAIL" <> help "Email address where administrative alerts will be sent")
171 <*> flag True False (long "disable-https" <> help "Disable automatic https configuration for the backend")
174data RemoteBuilder = RemoteBuilder_ObeliskVM
178 = DeployCommand_Init DeployInitOpts
179 | DeployCommand_Push (Maybe RemoteBuilder)
180 | DeployCommand_Test (PlatformDeployment, [String])
181 | DeployCommand_Update
184data DeployInitOpts = DeployInitOpts
185 { _deployInitOpts_outputDir :: FilePath
186 , _deployInitOpts_sshKey :: FilePath
187 , _deployInitOpts_hostname :: [String]
188 , _deployInitOpts_route :: String
189 , _deployInitOpts_adminEmail :: String
190 , _deployInitOpts_enableHttps :: Bool
194internalCommand :: Parser ObInternal
195internalCommand = subparser $ mconcat
196 [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
197 , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
200--TODO: Result should provide normalised path and also original user input for error reporting.
201thunkDirectoryParser :: Parser FilePath
202thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgument $ mconcat
205 , help "Path to directory containing thunk data"
208thunkConfig :: Parser ThunkConfig
209thunkConfig = ThunkConfig
211 ( flag' (Just True) (long "private" <> help "Mark thunks as pointing to a private repository")
212 <|> flag' (Just False) (long "public" <> help "Mark thunks as pointing to a public repository")
216thunkUpdateConfig :: Parser ThunkUpdateConfig
217thunkUpdateConfig = ThunkUpdateConfig
218 <$> optional (strOption (long "branch" <> metavar "BRANCH" <> help "Use the given branch when looking for the latest revision"))
221thunkPackConfig :: Parser ThunkPackConfig
222thunkPackConfig = ThunkPackConfig
223 <$> 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.")
227 = ThunkCommand_Update [FilePath] ThunkUpdateConfig
228 | ThunkCommand_Unpack [FilePath]
229 | ThunkCommand_Pack [FilePath] ThunkPackConfig
232thunkCommand :: Parser ThunkCommand
233thunkCommand = hsubparser $ mconcat
234 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> thunkUpdateConfig) $ progDesc "Update thunk to latest revision available"
235 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
236 , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> thunkPackConfig) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
241 { _shellOpts_shell :: String
242 , _shellOpts_command :: Maybe String
246shellFlags :: Parser String
248 flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
249 <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
250 <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
252shellOpts :: Parser ShellOpts
255 <*> optional (strArgument (metavar "COMMAND"))
257parserPrefs :: ParserPrefs
258parserPrefs = defaultPrefs
259 { prefShowHelpOnEmpty = True
262parseCLIArgs :: ArgsConfig -> [String] -> IO Args
263parseCLIArgs cfg = handleParseResult . execParserPure parserPrefs (argsInfo cfg)
265-- | Create an Obelisk config for the current process.
266mkObeliskConfig :: IO Obelisk
269 -- This function should not use argument parser (full argument parsing happens post handoff)
270 -- TODO: See if we can use the argument parser with a subset of the parsers to get logging level out.
271 let logLevel = toLogLevel $ any (`elem` ["-v", "--verbose"]) cliArgs
272 notInteractive <- not <$> isInteractiveTerm
273 cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
274 ObeliskError_ProcessError (ProcessFailure p code) ann ->
275 ( "Process exited with code " <> T.pack (show code) <> "; " <> reconstructCommand p
276 <> maybe "" ("\n" <>) ann
279 ObeliskError_Unstructured msg -> (msg, 2)
281 return $ Obelisk cliConf
283 toLogLevel = bool Notice Debug
284 isInteractiveTerm = do
285 isTerm <- hIsTerminalDevice stdout
286 -- Running in bash/fish/zsh completion
287 inShellCompletion <- liftIO $ isInfixOf "completion" . unwords <$> getArgs
289 -- Respect the user’s TERM environment variable. Dumb terminals
290 -- like Eshell cannot handle lots of control sequences that the
292 termEnv <- lookupEnv "TERM"
293 let isDumb = termEnv == Just "dumb"
295 return $ isTerm && not inShellCompletion && not isDumb
297-- | For use from development obelisk repls
300-- > runCommand $ someFuncInMonadObelisk ...
301runCommand :: ObeliskT IO a -> IO a
302runCommand f = flip runObelisk f =<< mkObeliskConfig
305main = runCommand . main' =<< getArgsConfig
307main' :: MonadObelisk m => ArgsConfig -> m ()
309 obPath <- liftIO getExecutablePath
310 myArgs <- liftIO getArgs
311 logLevel <- getLogLevel
312 putLog Debug $ T.pack $ unwords
313 [ "Starting Obelisk <" <> obPath <> ">"
314 , "args=" <> show myArgs
315 , "logging-level=" <> show logLevel
318 --TODO: We'd like to actually use the parser to determine whether to hand off,
319 --but in the case where this implementation of 'ob' doesn't support all
320 --arguments being passed along, this could fail. For now, we don't bother
321 --with optparse-applicative until we've done the handoff.
323 args' <- liftIO $ handleParseResult (execParserPure parserPrefs (argsInfo argsCfg) as)
324 case _args_noHandOffPassed args' of
326 True -> putLog Warning "--no-handoff should only be passed once and as the first argument; ignoring"
327 ob $ _args_command args'
328 handoffAndGo as = findProjectObeliskCommand "." >>= \case
329 Nothing -> go as -- If not in a project, just run ourselves
331 -- Invoke the real implementation, using --no-handoff to prevent infinite recursion
332 putLog Debug $ "Handing off to " <> T.pack impl
333 liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
335 "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
336 origPath:inPath:outPath:preprocessorName:packagePaths
337 | preprocessorName == preprocessorIdentifier && any (\c -> c == '.' || c == pathSeparator) origPath ->
338 ob $ ObCommand_Internal $ ObInternal_ApplyPackages origPath inPath outPath packagePaths
339 a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
340 | "--bash-completion" `isPrefixOf` a
341 && "--no-handoff" `elem` as -> go (a:as)
342 | otherwise -> handoffAndGo (a:as)
343 as -> handoffAndGo as
345ob :: MonadObelisk m => ObCommand -> m ()
347 ObCommand_Init source force -> initProject source force
348 ObCommand_Deploy dc -> case dc of
349 DeployCommand_Init deployOpts -> withProjectRoot "." $ \root -> do
350 let deployDir = _deployInitOpts_outputDir deployOpts
351 r <- liftIO $ canonicalizePath root
352 rootEqualsTarget <- liftIO $ equalFilePath r <$> canonicalizePath deployDir
353 when rootEqualsTarget $
354 failWith $ "Deploy directory " <> T.pack deployDir <> " should not be the same as project root."
355 thunkPtr <- readThunk root >>= \case
356 Left err -> failWith $ case err of
357 ReadThunkError_AmbiguousFiles ->
358 "Project root " <> T.pack r <> " is not a git repository or valid thunk"
359 ReadThunkError_UnrecognizedFiles ->
360 "Project root " <> T.pack r <> " is not a git repository or valid thunk"
361 _ -> "thunk read: " <> T.pack (show err)
362 Right (ThunkData_Packed ptr) -> return ptr
363 Right (ThunkData_Checkout (Just ptr)) -> return ptr
364 Right (ThunkData_Checkout Nothing) ->
365 getThunkPtr False root
366 let sshKeyPath = _deployInitOpts_sshKey deployOpts
367 hostname = _deployInitOpts_hostname deployOpts
368 route = _deployInitOpts_route deployOpts
369 adminEmail = _deployInitOpts_adminEmail deployOpts
370 enableHttps = _deployInitOpts_enableHttps deployOpts
371 deployInit thunkPtr deployDir sshKeyPath hostname route adminEmail enableHttps
372 DeployCommand_Push remoteBuilder -> do
373 deployPath <- liftIO $ canonicalizePath "."
374 deployPush deployPath $ case remoteBuilder of
376 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
377 DeployCommand_Update -> deployUpdate "."
378 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
380 ObCommand_Thunk tc -> case tc of
381 ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
382 ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
383 ThunkCommand_Pack thunks config -> for_ thunks (packThunk config)
384 ObCommand_Repl -> runRepl
385 ObCommand_Watch -> runWatch
386 ObCommand_Shell so -> withProjectRoot "." $ \root ->
387 projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
388 ObCommand_Doc shell' pkgs -> withProjectRoot "." $ \root ->
389 projectShell root False shell' (Just $ haddockCommand pkgs)
390 ObCommand_Internal icmd -> case icmd of
391 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case -- TODO: DELETE
392 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
395 liftIO $ runObelisk c $ deRefStaticPtr p
396 ObInternal_CLIDemo -> cliDemo -- TODO: DELETE
397 ObInternal_ApplyPackages origPath inPath outPath packagePaths -> do
398 liftIO $ Preprocessor.applyPackages origPath inPath outPath packagePaths
400haddockCommand :: [String] -> String
401haddockCommand pkgs = unwords
403 , unwords [getHaddockPath p ++ "/index.html" | p <- pkgs]
404 , "; do echo $p; done"
406 where getHaddockPath p = "$(ghc-pkg field " ++ p ++ " haddock-html --simple-output)"
408--TODO: Clean up all the magic strings throughout this codebase
410getArgsConfig :: IO ArgsConfig
411getArgsConfig = pure $ ArgsConfig { _argsConfig_enableVmBuilderByDefault = System.Info.os == "darwin" }
413encodeStaticKey :: StaticKey -> String
414encodeStaticKey = T.unpack . decodeUtf8With lenientDecode . Base16.encode . LBS.toStrict . Binary.encode
416-- TODO: Use failWith in place of fail to be consistent.
417decodeStaticKey :: String -> Either String StaticKey
418decodeStaticKey s = case Base16.decode $ encodeUtf8 $ T.pack s of
419 (b, "") -> case Binary.decodeOrFail $ LBS.fromStrict b of
420 Right ("", _, a) -> pure a
421 Right _ -> fail "decodeStaticKey: Binary.decodeOrFail didn't consume all input"
422 Left (_, _, e) -> fail $ "decodeStaticKey: Binary.decodeOrFail failed: " <> show e
423 _ -> 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