packages feed

duplo-1.8.0: src/CLI.hs

{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import           Control.Applicative             ((<$>))
import           Control.Exception               (Exception, catch, handle,
                                                  throw)
import qualified Control.Lens                    as LS
import           Control.Lens.Operators
import           Control.Monad                   (unless, void, when)
import           Data.ByteString.Base64          (decode)
import           Data.ByteString.Char8           (pack, unpack)
import           Data.List.Utils                 (replace)
import           Data.Maybe                      (fromMaybe)
import           Development.Duplo               (build)
import qualified Development.Duplo.Component     as CM
import           Development.Duplo.Server        (serve)
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           Development.Duplo.Utilities     (errorPrintSetter, logStatus)
import           Development.Duplo.Watcher       (watch)
import           Development.Shake               (ShakeException (..), cmd)
import           Development.Shake.FilePath      ((</>))
import qualified Filesystem.Path
import           GHC.Conc                        (forkIO)
import qualified GHC.IO
import           System.Console.GetOpt           (ArgDescr (..), ArgOrder (..),
                                                  OptDescr (..), getOpt)
import           System.Directory                (createDirectoryIfMissing,
                                                  doesFileExist,
                                                  getCurrentDirectory)
import           System.Environment              (getArgs, getExecutablePath,
                                                  lookupEnv)
import           System.FilePath.Posix           (takeDirectory)
import           System.Process                  (createProcess, proc,
                                                  waitForProcess)

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 </> "tests/"
  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
                    -- `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"       -> ( "test"    , ""      , "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

  -- Get all dependencies' names in dash-ified form
  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

  -- We should start with a clean slate.
  let noCleanActions = [ "clean", "bump", "init", "version" ]
  unless (cmdNameWithFlags `elem` noCleanActions) $ build "clean" [] buildConfig options

  -- 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 ""