packages feed

haskdogs-0.4.2: src/Main.hs

{-# LANGUAGE RecordWildCards, ScopedTypeVariables, ViewPatterns #-}
module Main (main) where

import Control.Monad
import Control.Applicative
import Control.Exception
import Data.Maybe
import Data.List
import System.IO
import System.Directory
import System.Process (readProcess)
import System.FilePath

import Options.Applicative
import qualified Options.Applicative as O
import Data.Version

{-
  ___        _   _
 / _ \ _ __ | |_(_) ___  _ __  ___
| | | | '_ \| __| |/ _ \| '_ \/ __|
| |_| | |_) | |_| | (_) | | | \__ \
 \___/| .__/ \__|_|\___/|_| |_|___/
      |_|
-}

data Opts = Opts {
    cli_dirlist_file :: FilePath
  , cli_filelist_file :: FilePath
  , cli_hasktags_args1 :: String
  , cli_ghc_pkgs_args :: String
  , cli_use_stack :: Tristate
  , cli_use_sandbox :: Tristate
  , cli_hasktags_args2 :: [String]
  } deriving(Show)

data Tristate = ON | OFF | AUTO
  deriving(Eq, Ord, Show, Read)

optsParser :: Parser Opts
optsParser = Opts
  <$> strOption (
        long "dir-list" <>
        short 'd' <>
        metavar "FILE" <>
        value "" <>
        help "File containing directory list to process" )
  <*> strOption (
        long "file-list" <>
        short 'f' <>
        metavar "FILE" <>
        value "" <>
        help "File containing Haskell sources to process" )
  <*> strOption (
        long "hasktags-args" <>
        metavar "OPTS" <>
        value "-c -x" <>
        help "Arguments to pass to hasktags")
  <*> strOption (
        long "ghc-pkg-args" <>
        metavar "OPTS" <>
        value "" <>
        help "Arguments to pass to ghc-pkgs")
  <*> option auto (
        long "use-stack" <>
        value AUTO <>
        help "Execute ghc-pkg via stack")
  <*> option auto (
        long "include-sandbox" <>
        value AUTO <>
        help "(!UNIMPLEMENTED!) Include .cabal-sandbox package databases")
  <*> many (argument str (metavar "OPTS" <> help "More hasktags options"))

exename, versionId :: String
exename = "haskdogs"
versionId = "0.4.0"

version :: Parser (a -> a)
version = infoOption (exename ++ " version " ++ versionId)
                     (long "version" <> help "Show version number")

opts = info (helper <*> version <*> optsParser)
      ( fullDesc <> header (exename ++ " - Recursive hasktags-based TAGS generator for a Haskell project" ))

{-
 __  __       _
|  \/  | __ _(_)_ __
| |\/| |/ _` | | '_ \
| |  | | (_| | | | | |
|_|  |_|\__,_|_|_| |_|

-}

main :: IO()
main = do

  Opts {..} <- execParser opts

  let
    -- Directory to unpack sources into
    getDataDir :: IO FilePath
    getDataDir = do
      x <- (</> ".haskdogs") <$> getHomeDirectory
      createDirectoryIfMissing False x
      return x

    cli_verbose = True

    vprint a
      | cli_verbose = eprint a
      | otherwise = return ()

    eprint = hPutStrLn stderr

    runp nm args inp = do
      vprint $ nm ++ " " ++ unwords args
      readProcess nm args inp

    -- Run GNU which tool
    checkapp :: String -> IO ()
    checkapp appname = do
      (runp "which" [appname] [] >> return ()) `onException`
        (eprint ("Please Install \"" ++ appname ++ "\" application"))

    hasapp :: String -> IO Bool
    hasapp appname = do
        vprint $ "Cheking for " ++ appname ++ " with GNU which"
        (runp "which" [appname] [] >> return True) `catch`
          (\(e::SomeException) -> vprint ("GNU which falied to find " ++ appname) >> return False)

  cwd <- getCurrentDirectory
  datadir <- getDataDir
  has_stack <- hasapp "stack"

  let

    readLinedFile f =
      lines <$> (hGetContents =<< (
        if (f=="-")
          then return stdin
          else openFile f ReadMode))

    readDirFile :: IO [FilePath]
    readDirFile
      | null cli_dirlist_file && null cli_filelist_file = return ["."]
      | null cli_dirlist_file = return []
      | otherwise = readLinedFile cli_dirlist_file

    readSourceFile :: IO [FilePath]
    readSourceFile
      | null cli_filelist_file = return []
      | otherwise = readLinedFile cli_filelist_file

    cli_hasktags_args = (words cli_hasktags_args1) ++ cli_hasktags_args2

    runp_ghc_pkgs args = go cli_use_stack where
      go ON = runp "stack" (["exec", "ghc-pkg", "--"] ++ args) []
      go OFF = runp "ghc-pkg" args []
      go AUTO = if has_stack then go ON else go OFF

    cabal_or_stack = go cli_use_stack where
      go ON = "stack"
      go OFF = "cabal"
      go AUTO = if has_stack then go ON else go OFF

    -- Finds *hs in dirs, but filter-out Setup.hs
    findSources :: [FilePath] -> IO [FilePath]
    findSources dirs =
      filter (not . isSuffixOf "Setup.hs") . lines <$>
      runp "find" (dirs ++ words "-type f -and ( -name *\\.hs -or -name *\\.lhs -or -name *\\.hsc )") []

    grepImports :: String -> Maybe String
    grepImports line = case words line of
        ("import":"qualified":x:_) -> Just x
        ("import":x:_) -> Just x
        _ -> Nothing

    -- Produces list of imported modules for file.hs given
    findModules :: [FilePath] -> IO [String]
    findModules files = (catMaybes . map grepImports . lines) <$> runp "cat" files []

    -- Maps import name to haskell package name
    iname2module :: String -> IO (Maybe String)
    iname2module iname = do
        mod <- (listToMaybe . words) <$> (runp_ghc_pkgs ["--simple-output", "find-module", iname])
        vprint $ "Import " ++ iname ++ " resolved to " ++ (fromMaybe "NULL" mod)
        return mod

    inames2modules :: [String] -> IO [FilePath]
    inames2modules is = nub . sort . catMaybes <$> forM (nub is) iname2module

    -- Unapcks haskel package to the sourcedir
    unpackModule :: FilePath -> IO (Maybe FilePath)
    unpackModule ((datadir</>) -> p) = do
        exists <- doesDirectoryExist (datadir</>p)
        case exists of
          True ->  do
            vprint $ "Already unpacked " ++ p
            return (Just p)
          False -> do
            bracket_ (setCurrentDirectory datadir) (setCurrentDirectory cwd) $
              ( runp cabal_or_stack ["unpack", p] [] >> return (Just p)
              ) `catch`
              (\(_ :: SomeException) ->
                eprint ("Can't unpack " ++ p) >> return Nothing
              )

    unpackModules :: [FilePath] -> IO [FilePath]
    unpackModules ms = catMaybes <$> mapM unpackModule ms

    gentags :: IO ()
    gentags = do
      checkapp "hasktags"
      files <- do
        dirs <- readDirFile
        ss_local <- (++) <$> readSourceFile <*> findSources dirs
        when (null ss_local) $ do
          fail $ "Haskdogs were not able to find any sources in " <> (intercalate ", " dirs)
        ss_l1deps <- findModules ss_local >>= inames2modules >>= unpackModules >>= findSources
        return $ ss_local ++ ss_l1deps
      runp "hasktags" (cli_hasktags_args ++ files) []
      putStrLn "\nSuccess"

  {- _real_main_ -}
  gentags