packages feed

hup-0.2.0.0: src/Main.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE CPP #-}

module Main where

import Prelude hiding (FilePath)
import qualified Prelude

import Control.Monad.IO.Class     ( MonadIO(..) )
import Control.Monad.Reader       (MonadReader(..), runReaderT, liftM)
import Control.Monad.Trans        (lift)
import Control.Monad.Trans.Maybe  (MaybeT(..))
import Control.Monad.Trans.Except (ExceptT(..),runExceptT,throwE)
import Data.Char                  (isSpace, isDigit)
import Data.Either                (either)
import Data.List                  (isPrefixOf)
import Data.Maybe                 (isNothing)
import Data.Text                  ( Text )
import qualified Data.Text as T
import Data.Monoid                ( (<>) )
import Shelly.Lifted
import System.Directory           -- (listDirectory)
import System.IO                  (hSetBuffering, BufferMode( LineBuffering )
                                  , stdout)

import Distribution.Hup           ( Package(..),IsDocumentation(..)
                                  ,IsCandidate(..),Auth(..),Upload(..)
                                  ,mkAuth ,buildTar
                                  ,readCabal, extractCabal
                                  ,parseTgzFilename' 
                                  , getUploadUrl)
import CmdArgs                    (HupCommands(..), isUpload, processArgs)
import SanityCheck                (sanity)
import Upload                     (doUpload)

#if MIN_VERSION_directory(1,2,5)
#else
listDirectory :: Prelude.FilePath -> IO [Prelude.FilePath]
listDirectory path =
  filter f <$> getDirectoryContents path
  where f filename = filename /= "." && filename /= ".."
#endif

-- | just a convenience alias, short for @'MonadIO' m, 'MonadSh' m,
--  'MonadShControl' m
type MonadShellish m = (MonadIO m, MonadSh m, MonadShControl m) 

-- | just a convenience alias, short for @'MonadIO' m, 'MonadSh' m,
-- 'MonadShControl' m, 'MonadReader' 'HupCommands' m@
type MonadHup m = (MonadIO m, MonadSh m, MonadShControl m, MonadReader HupCommands m)

rstrip :: Text -> Text
rstrip = T.dropWhileEnd isSpace 

-- | does haddock have ability to do hyperlinked source.
haddockCanHyperlinkSrc :: MonadShellish m  => m Bool
haddockCanHyperlinkSrc = errExit False $ do
  run_ "haddock" ["--hyperlinked-source"]
  (==0) `liftM` lastExitCode

-- | safe version of "maximum"
maxMay xs = if null xs then Nothing else Just $ maximum xs

-- | look for ghc & try to add it to path
addGhcPath :: MonadShellish m => m ()
addGhcPath = 
  -- see if ghc is already in path;
  -- if not, try looking for it in stack's "programs" directory.
  whenM (isNothing `liftM` which "ghc") $ do
    progPath <- rstrip <$> silently (run "stack" ["path", "--programs"])
    let myFilt x = "ghc" `isPrefixOf` x && isDigit (last x)
    ghcDir <- liftIO $ (maxMay . filter myFilt) `liftM` listDirectory (T.unpack progPath)
    case ghcDir of
      Nothing -> terror "couldn't find ghc on path, please add it"
      Just ghcDir' -> appendToPath $ progPath </> ghcDir' </> "bin"


-- | Run commands for building documentation tgz. A Haskellified version of
-- phadej's script at
--    https://github.com/phadej/binary-orphans/blob/master/hackage-docs.sh,
-- which is a stack-enabled version of ekmett's script at
--    https://github.com/ekmett/lens/blob/master/scripts/hackage-docs.sh.
--
-- `stackBuildDocs dir pkg CandidatePkg` will build documentation in the
-- directory `dir`, and will return an `Upload` object, which can be uploaded
-- with `Distribution.Hup.Upload.doUpload`.
--
-- Requires that stack, haddock and cabal be on the path.
--
-- Sample usage:
--
-- > import Distribution.Hup.Upload 
-- > :set -XOverloadedStrings 
-- > let p = Package "foo" "0.1"
-- > upload <- shelly $ stackBuildDocs "." p CandidatePkg 
-- > doUpload "http://localhost:8080" upload (mkAuth "myname" "mypass") 
--
-- When running from within ghci, you may have to unset some
-- environment variables that have been set.
--
-- > import System.Environment
-- > import Control.Monad
-- > mapM_ unsetEnv ["HASKELL_PACKAGE_SANDBOXES", "GHC_PACKAGE_PATH", "HASKELL_PACKAGE_SANDBOX", "STACK_EXE", "HASKELL_DIST_DIR"]
--
stackBuildDocs :: MonadHup m => FilePath -> Package -> m Upload
stackBuildDocs dir (Package pkg ver) = do
  hc <- ask
  -- work out flags to call with ...
  canHyperlink <- haddockCanHyperlinkSrc 
  let hyperlinkFlag = if canHyperlink
                      then ["--haddock-option=--hyperlinked-source"]
                      else []
  unless (quick hc) $ do
    echo "build dependencies docs"
    run_ "stack" ["haddock", "--only-dependencies"]
  snapshotpkgdb <- rstrip <$> silently (run "stack" ["path", "--snapshot-pkg-db"])
  localpkgdb    <- rstrip <$> silently (run "stack" ["path", "--local-pkg-db"])
  let verboseCommands  = if verbose hc then ["-v2"] else [] 
  let haddockExtraArgs = let args = haddockArgs hc 
                         in (if null args
                            then []
                            else ["--haddock-options=" <>T.pack(haddockArgs hc)])
                          ++ (if executables hc then ["--executables"] else [])
                          ++(if tests hc then ["--tests"] else [])
                          ++(if internal hc then ["--internal"] else [])
  let cabalExtraArgs =(if tests hc then ["--enable-tests"] else [])
  echo "configuring"
  let builddir= toTextIgnore $ dir </> "dist"
  run_ "cabal" $["configure", "--builddir="<>builddir, 
               "--package-db=clear", "--package-db=global", 
               "--package-db=" <> snapshotpkgdb, 
               "--package-db=" <> localpkgdb] ++ verboseCommands
               ++ cabalExtraArgs
  echo "making docs"
  run_ "cabal" $["haddock", "--builddir=" <> builddir,  
               "--html-location=/package/$pkg-$version/docs",
               "--contents-location=/package/$pkg-$version"] 
               ++ hyperlinkFlag ++ verboseCommands
               ++ haddockExtraArgs 
  pkg <- return $ T.pack pkg
  ver <- return $ T.pack ver
  let srcDir = builddir </> "doc" </> "html" </> pkg
      tgtDir = dir </> (pkg <> "-" <> ver <> "-docs")
  cp_r srcDir tgtDir
  echo "tarring"
  let
    up = T.unpack
    fromPath = T.unpack . toTextIgnore 
    docTgz = fromPath $ dir </> (pkg<>"-"<>ver <> "-docs.tar.gz") 
    docDir = pkg <> "-" <> ver <> "-docs" 
  -- or, if you have tar on system, could use:
  --    run "tar" ["cvz", "-C", dir, "--format=ustar", "-f", docTgz,
  --                pkg <> "-" <> ver <> "-docs" ]
  liftIO $ buildTar docTgz (fromPath dir) [up docDir]
  return $ Upload (Package (up pkg) (up ver)) docTgz Nothing IsDocumentation (isCand hc)


-- | if we have a username, then we need to get
-- a password, either from the command-line or the env
getAuth :: MonadSh m => HupCommands -> m (Maybe Auth)
getAuth hc = runMaybeT $ do
  hc <- MaybeT $ return $ isUpload hc
  u <- MaybeT $ return $ user hc 
  case password hc of
    Just p -> MaybeT $ return $ mkAuth u p
    Nothing -> do x <- get_env "PASSWORD"
                  case x of
                    Nothing -> terror "username specified, but no password"
                    Just p  -> MaybeT $ return $ mkAuth u (T.unpack p)

 

-- Use for "early return"
data Done = Done
  deriving (Show)

type MonadDone m a = ExceptT Done m a 

runEarlyReturn :: Monad m => MonadDone m () -> m ()
runEarlyReturn f = 
  either (const ()) id <$> runExceptT f

-- exit early
done :: Monad m => ExceptT Done m a
done = throwE Done

isCand hc = 
  if candidate hc
  then CandidatePkg
  else NormalPkg 


fromString = fromText . T.pack

-- | check that stack, cabal & haddock are on path
checkPrereqs :: MonadSh m => m ()
checkPrereqs = do
  whenM (isNothing <$> which "stack") $  do
    echo "Couldn't find stack on path - do you need to install stack?"
    quietExit 1
  whenM (isNothing <$> which "cabal") $ do
    echo $ T.unwords ["Couldn't find cabal on path - do you need to"
                      ,"run 'stack install cabal-install'? Exiting"]
    quietExit 1
  whenM (isNothing <$> which "haddock") $ do
    echo $ T.unwords ["Couldn't find haddock on path - do you need to"
                      ,"run 'stack install haddock'? Exiting"]
    quietExit 1


-- | Look at a FILE command-line arg of something we've been asked to
-- upload, & try uploading it.
--
-- Will throw exceptions if the file doesn't exist, or doesn't look
-- like a .tar.gz file, or if we've been asked to upload docco &
-- it looks like a source file.
--
-- If the upload fails due to a bad status, however, it should
-- give a hopefully comprehensible message then end early.
--
-- todo: give nice error messages, rather than throwing exceptions
-- in some cases?
uploadTgz :: 
    (MonadSh m, MonadIO m, MonadReader HupCommands m) =>
    IsDocumentation -> Text -> MonadDone m ()
uploadTgz expectedType desc = do 
  hc <- ask
  let fileName   = file hc 
      fileName'' = T.pack fileName
      candType   = isCand hc 
      serverUrl  = server hc
      verb       = verbose hc
  (upType, Package pkg ver) <- let parsed = parseTgzFilename' fileName
                               in either (lift . terror) return parsed 
  when (upType /= expectedType) $
    lift $ terror $ T.unwords ["Expected", desc, "file, got", fileName'']
  -- if all is ok, do the upload.
  let upload = Upload (Package pkg ver) (file hc) Nothing expectedType candType 
  auth <- lift $ getAuth hc
  let url = getUploadUrl serverUrl upload
  lift $ echo $ "uploading to " <> T.pack url
  serverResponse <- liftIO $ doUpload serverUrl upload auth
  let displayedMesg msg = "Uploaded successfully" <>
                            (if verb
                            then T.pack msg
                            else "")
  case serverResponse of 
    Left err -> do lift $ echo $ "Error from server:\n" <> T.pack err
                   throwE Done 
    Right msg  -> lift $ echo $ displayedMesg msg



-- | Run a hup command (which contains details of server url to use,
-- user authentication details, etc.)
--
-- sample usage:
--
-- > let d = Docbuild { verbose == True }
-- > shelly $ verbosely $ runReaderT $ mainSh d
mainSh :: MonadHup m => m ()
mainSh =  do  
  hc <- ask
  withTmpDir $ \tmpDir -> 
    runEarlyReturn $ do
      cabalConts <- liftIO readCabal
      let packageName = extractCabal "name" cabalConts
          packageVer  = extractCabal "version" cabalConts 
      case hc of
        Packup {}   -> do uploadTgz IsPackage "package"  
                          throwE Done
        Docup  {}   -> do uploadTgz IsDocumentation "documentation"
                          throwE Done
        _           -> return () -- i.e. carry on.
      -- if still here, we've been asked to do a build.
      uploadable <- do let p = Package packageName packageVer
                       buildRes <- lift $ stackBuildDocs tmpDir p  
                       let tgzFile = fromText $ T.pack $ fileToUpload buildRes 
                       case hc of 
                         Docbuild {} -> lift (cp tgzFile ".") >>
                                        throwE Done
                         _           -> return buildRes
      auth     <- lift $ getAuth hc
      let url = getUploadUrl (server hc) uploadable
      lift $ echo $ "uploading to " <> T.pack url
      response <- liftIO $ doUpload (server hc) uploadable auth
      case response of 
        Left err -> do lift $ echo $ "Error from server:\n'" <> T.pack err
                       throwE Done 
        Right msg -> lift $ do echo "Uploaded successfully"
                               echo $ "mesg was: " <> T.pack msg



main :: IO ()
main = do
  hSetBuffering stdout LineBuffering
  hupCommand <- sanity =<< processArgs
  let verbosify = if verbose hupCommand
                  then verbosely
                  else id
  shelly $ do
    silently checkPrereqs
    verbosify $ do 
      addGhcPath
      runReaderT mainSh hupCommand 
  return ()