duplo-1.6.9: src/CLI.hs
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import Control.Applicative ((<$>))
import Control.Exception (catch, handle, throw, Exception)
import Control.Lens.Operators
import Control.Monad (void, when, unless)
import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
import Data.ByteString.Base64 (decode)
import Data.ByteString.Char8 (pack, unpack)
import Data.Maybe (fromMaybe)
import Development.Duplo (build)
import Development.Duplo.Server (serve)
import Development.Duplo.Utilities (logStatus, errorPrintSetter, replace)
import Development.Duplo.Watcher (watch)
import Development.Shake (cmd, ShakeException(..))
import Development.Shake.FilePath ((</>))
import GHC.Conc (forkIO)
import System.Console.GetOpt (getOpt, OptDescr(..), ArgDescr(..), ArgOrder(..))
import System.Directory (getCurrentDirectory, createDirectoryIfMissing, doesFileExist)
import System.Environment (lookupEnv, getArgs, getExecutablePath)
import System.FilePath.Posix (takeDirectory)
import System.Process (proc, createProcess, waitForProcess)
import qualified Control.Lens
import qualified Development.Duplo.Component as CM
import qualified Development.Duplo.Types.AppInfo as AI
import qualified Development.Duplo.Types.Builder as TB
import qualified Development.Duplo.Types.Config as TC
import qualified Development.Duplo.Types.Options as OP
import qualified Filesystem.Path
import qualified GHC.IO
main :: IO ()
main = do
-- Command-line arguments
args <- getArgs
let (cmdName:cmdArgs) =
if not (null args)
-- Normal case (with command name)
then args
-- Edge cases (with nothing provided)
else [""]
-- Deal with options
let (actions, nonOptions, errors) = getOpt Permute OP.options args
options <- foldl (>>=) (return OP.defaultOptions) actions
-- Port for running dev server
port <- fmap read $ fromMaybe "8888" <$> lookupEnv "PORT"
-- Environment - e.g. dev, staging, production
duploEnvMB <- lookupEnv "DUPLO_ENV"
-- Build mode, for dependency selection
duploMode <- fromMaybe "" <$> lookupEnv "DUPLO_MODE"
-- Application parameter
duploIn <- fromMaybe "" <$> lookupEnv "DUPLO_IN"
-- Current working directory
cwd <- getCurrentDirectory
-- Duplo directory, assuming this is a build cabal executable (i.e.
-- `./dist/build/duplo/duplo`)
duploPath <- fmap ((</> "../../../") . takeDirectory) getExecutablePath
-- Base64 decode
let duploInDecoded = case decode $ pack duploIn of
Left _ -> ""
Right input -> unpack input
-- Paths to various relevant directories
let nodeModulesPath = duploPath </> "node_modules/.bin/"
let utilPath = duploPath </> "util/"
let distPath = duploPath </> "dist/build/"
let miscPath = duploPath </> "etc/"
let defaultsPath = miscPath </> "static/"
let appPath = cwd </> "app/"
let devPath = cwd </> "dev/"
let testPath = cwd </> "test/"
let assetsPath = appPath </> "assets/"
let depsPath = cwd </> "components/"
let targetPath = cwd </> "public/"
-- Extract environment
let duploEnv = case cmdName of
-- `build` is a special case. It takes `production` as the
-- default.
"build" -> fromMaybe "production" duploEnvMB
-- By default, `dev` is the default.
_ -> fromMaybe "development" duploEnvMB
-- Internal command translation
let (cmdNameTranslated, bumpLevel, buildMode, toWatch) =
case cmdName of
"info" -> ( "version" , "" , duploEnv , False )
"ver" -> ( "version" , "" , duploEnv , False )
"new" -> ( "init" , "" , duploEnv , False )
"bump" -> ( "bump" , "patch" , duploEnv , False )
"release" -> ( "bump" , "patch" , duploEnv , False )
"patch" -> ( "bump" , "patch" , duploEnv , False )
"minor" -> ( "bump" , "minor" , duploEnv , False )
"major" -> ( "bump" , "major" , duploEnv , False )
"dev" -> ( "build" , "" , "development" , True )
"live" -> ( "build" , "" , "production" , True )
"production" -> ( "build" , "" , "production" , True )
"build" -> ( "build" , "" , duploEnv , False )
"test" -> ( "build" , "" , "test" , False )
_ -> ( cmdName , "" , duploEnv , False )
-- Certain flags turn into commands.
let cmdNameWithFlags = if OP.optVersion options
then "version"
else cmdNameTranslated
-- Display version either via command or option. We need to do this
-- before any `readManifest` as it throws an error when there isn't one,
-- as it should.
when (cmdNameWithFlags == "version") $ do
let command = utilPath </> "display-version.sh"
let process = createProcess $ proc command [duploPath]
-- Run the command.
(_, _, _, handle) <- process
-- Remember to do it synchronously.
void $ waitForProcess handle
-- We only care about exceptions thrown by the builder.
let ignoreManifestError' (e :: TB.BuilderException) = case e of
-- Only when missing manifest
TB.MissingManifestException _ -> return ""
-- Re-throw other builder exceptions.
_ -> throw e
-- Helper function to ignore exceptions, only for this stage, before
-- Shake is run.
let ignoreManifestError io = catch io ignoreManifestError'
-- Gather information about this project
appName <- ignoreManifestError $ fmap AI.name CM.readManifest
appVersion <- ignoreManifestError $ fmap AI.version CM.readManifest
appId <- ignoreManifestError $ fmap CM.appId CM.readManifest
-- We may need custom builds with mode
let depManifestPath = cwd </> "component.json"
dependencies <- CM.getDependencies $ case duploMode of
"" -> Nothing
a -> Just a
let depIds = fmap (replace "/" "-") dependencies
-- Display additional information when verbose.
when (OP.optVerbose options) $
-- More info about where we are
putStr $ "\n"
++ ">> Current Directory\n"
++ "Application name : "
++ appName ++ "\n"
++ "Application version : "
++ appVersion ++ "\n"
++ "Component.IO repo ID : "
++ appId ++ "\n"
++ "Current working directory : "
++ cwd ++ "\n"
++ "duplo is installed at : "
++ duploPath ++ "\n"
-- Report back what's given for confirmation.
++ "\n"
++ ">> Environment Variables\n"
++ "DUPLO_ENV (runtime environment) : "
++ duploEnv ++ "\n"
++ "DUPLO_MODE (build mode) : "
++ duploMode ++ "\n"
++ "DUPLO_IN (app parameters) : "
++ duploInDecoded ++ "\n"
-- Construct environment
let buildConfig = TC.BuildConfig
{ TC._appName = appName
, TC._appVersion = appVersion
, TC._appId = appId
, TC._cwd = cwd
, TC._duploPath = duploPath
, TC._env = duploEnv
, TC._mode = duploMode
, TC._nodejsPath = nodeModulesPath
, TC._dist = distPath
, TC._input = duploIn
, TC._utilPath = utilPath
, TC._miscPath = miscPath
, TC._defaultsPath = defaultsPath
, TC._appPath = appPath
, TC._devPath = devPath
, TC._testPath = testPath
, TC._assetsPath = assetsPath
, TC._depsPath = depsPath
, TC._targetPath = targetPath
, TC._bumpLevel = bumpLevel
, TC._port = port
, TC._dependencies = depIds
, TC._buildMode = buildMode
}
-- If there is a Makefile, run that as well, with the environment as the
-- target (e.g. `duplo dev` would run `make development` and `duplo build` would
-- run `make production`).
makefileExists <- doesFileExist $ cwd </> "Makefile"
when makefileExists $ void $ createProcess $ proc "make" [duploEnv]
-- Construct the Shake command.
let shake' = build cmdNameWithFlags cmdArgs buildConfig options
let shake = shake' `catch` handleExc
-- Watch or just build.
unless toWatch shake
when toWatch $ do
-- Start a local server.
_ <- forkIO $ serve port
-- Only watch the dev and the app directories. We're not watching the
-- dependency directory because it triggers a race condition with
-- componentjs.
let targetDirs = [devPath, appPath]
-- Make sure we have these directories to watch.
mapM_ (createDirectoryIfMissing True) targetDirs
-- Watch for file changes.
watch shake targetDirs
-- | Handle all errors.
handleExc (e :: ShakeException) = do
putStr $ show e
logStatus errorPrintSetter "Build failed"
putStrLn ""