packages feed

cabal-auto-expose-0.1.0.1: src/Distribution/Simple/AutoExpose.hs

-- | Import this module in your @Setup.hs@ to auto detect library modules in
-- your project. The API does not conceal it's internals but in most cases you
-- should only need the functions and datatype under
-- 'Quick Start Functions' ("Distribution.Simple.AutoExpose#QuickStartFunctions").
-- For more granular access the ones under
-- 'Internal Functions' ("Distribution.Simple.AutoExpose#InternalFunctions") are
-- available but subject to change.

module Distribution.Simple.AutoExpose where

import Control.Exception(catch,IOException)
import Control.Monad((>=>),filterM)
import Control.Monad.Extra(ifM,notM,whenJust)
import Data.List(intercalate,nub)
import Distribution.Compat.Lens((%~),(&))
import Distribution.ModuleName(ModuleName,fromString,validModuleComponent)
import Distribution.PackageDescription(hsSourceDirs,buildInfo,testBuildInfo,benchmarkBuildInfo,executables,testSuites,benchmarks,libBuildInfo,subLibraries,library,Library,GenericPackageDescription(..),HookedBuildInfo,Executable,TestSuite,Benchmark,condTreeData,packageDescription)
import Distribution.Simple.BuildPaths(autogenPathsModuleName)
import Distribution.Simple.PreProcess(knownSuffixHandlers)
import Distribution.Simple.Setup(BuildFlags, ReplFlags, HscolourFlags, HaddockFlags, CopyFlags, InstallFlags, TestFlags, BenchmarkFlags, RegisterFlags, DoctestFlags, ConfigFlags,fromFlag, configVerbosity)
import Distribution.Simple.UserHooks(UserHooks,Args,hookedPreProcessors, buildHook,replHook,hscolourHook,doctestHook,haddockHook,copyHook,instHook,testHook,benchHook,regHook,unregHook,confHook)
import Distribution.Simple.Utils(findPackageDesc,notice)
import Distribution.Types.LocalBuildInfo(LocalBuildInfo)
import Distribution.Types.PackageDescription(PackageDescription,package)
import GHC.Stack(HasCallStack)
import System.Directory(makeAbsolute,listDirectory,doesDirectoryExist,withCurrentDirectory,pathIsSymbolicLink,getTemporaryDirectory)
import System.FilePath(splitDirectories, dropExtension, takeExtension,equalFilePath,makeRelative,(</>),(<.>))
import qualified Distribution.Simple(defaultMainWithHooks,simpleUserHooks)
import qualified Distribution.Types.BuildInfo.Lens as L
import qualified Distribution.Types.Library.Lens as L
import Distribution.Pretty(prettyShow)
import Distribution.Types.PackageId(PackageIdentifier(pkgName,pkgVersion))
import Distribution.Types.Version()
import Distribution.PackageDescription.PrettyPrint(writeGenericPackageDescription)
import Data.Maybe(fromMaybe)

-- * Quick Start Functions #QuickStartFunctions#

-- | The common case top level function where this library is the only custom part of your project
--
-- > import qualified Distribution.Simple.AutoExpose
-- > main = AutoExpose.defaultMain
defaultMain :: IO ()
defaultMain = defaultMainWithHooks Distribution.Simple.simpleUserHooks

-- | If you have already using custom 'UserHooks' use this in your Setup.hs's 'main'
--
-- > import qualified Distribution.Simple.AutoExpose as AutoExpose
-- > main = AutoExpose.defaultMainWithHooks myHooks
defaultMainWithHooks :: UserHooks -> IO ()
defaultMainWithHooks uhs = Distribution.Simple.defaultMainWithHooks (autoExposeHooks Nothing uhs)

-- | The common case top level function where this library is the only custom part of your project
--
-- It also generates an explicit Cabal file at @\/<system-temp-directory>\/<package-name>-<package-version>-generated.cabal@
--
-- > import qualified Distribution.Simple.AutoExpose
-- > main = AutoExpose.defaultMainGenerateCabal
defaultMainGenerateCabal :: IO ()
defaultMainGenerateCabal = do
  defaultCabalWriter <- defaultWriteGeneratedCabal
  defaultMainWithHooksGenerateCabal defaultCabalWriter Distribution.Simple.simpleUserHooks

-- | If you have already using custom 'UserHooks' use this in your Setup.hs's 'main' and also
-- provide a way to generate an explicit Cabal file.
--
-- > import qualified Distribution.Simple.AutoExpose as AutoExpose
-- > main = do
-- >   cabalWriter <- defaultWriteGeneratedCabal
-- >   AutoExpose.defaultMainWithHooksGenerateCabal cabalWriter myHooks
defaultMainWithHooksGenerateCabal :: WriteGeneratedCabal -> UserHooks -> IO ()
defaultMainWithHooksGenerateCabal writeGeneratedCabal uhs =
  Distribution.Simple.defaultMainWithHooks (autoExposeHooks (Just writeGeneratedCabal) uhs)

-- | A datatype that wraps a function that outputs the name of the
-- explicity generated Cabal file and an absolute path to a directory
-- into which to write it.
data WriteGeneratedCabal =
  WriteGeneratedCabal
  { writeGeneratedCabalPath :: FilePath
  , writeGeneratedCabalName :: GenericPackageDescription -> FilePath
  }

-- | Write the Cabal file to the system temp directory by default using
-- 'defaultGeneratedCabalName' for the filename.
defaultWriteGeneratedCabal :: IO WriteGeneratedCabal
defaultWriteGeneratedCabal = do
  tmp <- getTemporaryDirectory
  pure (WriteGeneratedCabal tmp defaultGeneratedCabalName)

-- * Internal Functions #InternalFunctions#

-- | Search for file paths that look like valid modules and convert to the
--   components to the Cabal internal 'ModuleName'.
moduleNamesToExpose
  :: [String] -- ^ File extensions of valid Haskell modules, includes pre-processor extensions
  -> [FilePath] -- ^ File paths to search
  -> [ModuleName]
moduleNamesToExpose extensions =
  map (fromString . intercalate ".")
  . filter (all validModuleComponent)
  . map toModuleComponents
  . filter hasExtension
  where
    hasExtension :: FilePath -> Bool
    hasExtension f =
      elem
        (drop 1 (takeExtension f)) -- 'takeExtension' preserves the '.' so drop it
        extensions
    toModuleComponents :: FilePath -> [String]
    toModuleComponents =
      splitDirectories . dropExtension

-- | Recursively collect the files in a directory, optionally excluding some
-- files. Symlinks are ignored and collected paths are relative to the search
-- directory, eg. if the search directory is @\/home\/user\/myproject\/src@ the path
-- @\/home\/user\/myproject\/src\/A\/B\/C.hs@ is returned as @A\/B\/C.hs@ so it can
-- converted by 'moduleNamesToExpose' to a valid module name.
getDirectoryContents
  :: HasCallStack
  => FilePath -- ^ Directory to search (path must be absolute)
  -> [FilePath] -- ^ Paths to ignore (paths must be absolute)
  -> IO [FilePath] -- ^ File paths made relative to the search directory
getDirectoryContents dir excludedDirs = do
  (map (makeRelative dir)) <$> go [dir] []
  where
    go :: [FilePath] -> [FilePath] -> IO [FilePath]
    go (f:fs) accum
      | any (equalFilePath f) excludedDirs = go fs accum
      | otherwise =
          ifM (doesDirectoryExist f)
            (catch
              (withCurrentDirectory f $ do
                 contents <-
                   (listDirectory >=> filterM (notM . pathIsSymbolicLink) >=> mapM makeAbsolute) f
                 go (contents ++ fs) accum)
              (\(_ :: IOException) -> go fs accum))
            (go fs (f:accum))
    go [] accum = pure accum

-- | Captures the detected Haskell modules and signatures in a library
data ExposedLib =
  ExposedLib
  { exposedLibModules :: [ModuleName]
  , exposedLibSignatures :: [ModuleName]
  }
  deriving Show

-- | All the exposed library components, main library and sub libraries
data AllExposed =
  AllExposed
  { allExposedMainLib :: ExposedLib
  , allExposedSubLibs :: [(Library,ExposedLib)]
  }
  deriving Show

-- | The common pieces of 'GenericPackageDescription' and 'PackageDescription'
-- which we need to auto detect Haskell modules /and/ signatures. We can't make
-- do with just the 'PackageDescription' because the 'confHook' which
-- instantiates the library with signatures only has access to
-- 'GenericPackageDescription'.
data PackageDescriptionSubset =
  PackageDescriptionSubset
  { packageDescriptionSubsetLibrary :: Maybe Library
  , packageDescriptionSubsetSubLibraries :: [Library]
  , packageDescriptionSubsetExecutables :: [Executable]
  , packageDescriptionSubsetTestSuites :: [TestSuite]
  , packageDescriptionSubsetBenchmarks :: [Benchmark]
  }
  deriving Show

genericPackageDescriptionToSubset :: GenericPackageDescription -> PackageDescriptionSubset
genericPackageDescriptionToSubset gpd =
  PackageDescriptionSubset
    (fmap condTreeData (condLibrary gpd))
    (map (condTreeData . snd) (condSubLibraries gpd))
    (map (condTreeData . snd) (condExecutables gpd))
    (map (condTreeData . snd) (condTestSuites gpd))
    (map (condTreeData . snd) (condBenchmarks gpd))

packageDescriptionToSubset :: PackageDescription -> PackageDescriptionSubset
packageDescriptionToSubset pd =
  PackageDescriptionSubset
    (library pd)
    (subLibraries pd)
    (executables pd)
    (testSuites pd)
    (benchmarks pd)

-- | Source directories for all non-library components (benchmarks, executables
-- etc.) so that we can exclude their modules if they appear inside the library's
-- source directory
nonLibraryHsSourcePaths :: PackageDescriptionSubset -> [[FilePath]]
nonLibraryHsSourcePaths pds =
  map hsSourceDirs $
     (map buildInfo (packageDescriptionSubsetExecutables pds))
  ++ (map testBuildInfo (packageDescriptionSubsetTestSuites pds))
  ++ (map benchmarkBuildInfo (packageDescriptionSubsetBenchmarks pds))

-- | Associate each item in a list will it's left and right elements, eg.
-- > indexWithNeighbors [1,2,3,4] == [(1,[2,3,4]),(2,[1,3,4]),(3,[1,2,4]),(4,[1,2,3])]
--
-- Used to associate a source directory with possibly inner directories that
-- should be ignored with searching for Haskell modules.
indexWithNeighbors :: [a] -> [(a,[a])]
indexWithNeighbors (a:as) = reverse (go [] a as [])
  where
    go [] x (r:rs) accum = go [x] r rs ((x,(r:rs)):accum)
    go ls x (r:rs) accum = go (ls++[x]) r rs ((x,(ls++(r:rs))):accum)
    go ls x [] accum = (x,ls):accum
indexWithNeighbors [] = []

-- | Drill into the source trees for a component and find modules
-- excluding the source trees for other components
getExposedModules
  :: HasCallStack
  => [String] -- ^ Known Haskell and/or preprocessor extensions
  -> [FilePath] -- ^ The source trees for this component
  -> [[FilePath]] -- ^ The source trees for all the other components
  -> IO [ModuleName] -- ^ Detect modules in this component
getExposedModules exts hsSrcDirs otherHsSrcDirs = do
  absHsSrcDirs <- mapM makeAbsolute hsSrcDirs
  absOtherHsSrcDirs <- mapM makeAbsolute (concat otherHsSrcDirs)
  contents <-
    concat
    <$> ( mapM (\(srcDir,excludeDirs) -> getDirectoryContents srcDir excludeDirs)
          $ map (\(srcDir,otherSrcDirs) -> (srcDir,otherSrcDirs ++ absOtherHsSrcDirs))
          $ indexWithNeighbors absHsSrcDirs
        )
  pure $ moduleNamesToExpose exts contents

-- | Get all the exposed modules and signatures in this project's main and sub libraries
getAllExposed
  :: HasCallStack
  => PackageDescriptionSubset
  -> [String] -- ^ Custom preprocessor extensions
  -> IO AllExposed
getAllExposed pds customPPExts =
  case (packageDescriptionSubsetLibrary pds) of
    Nothing -> do
      let mainLib = ExposedLib [] []
      subLibs <- subLibsExposed []
      pure (AllExposed mainLib subLibs)
    Just l -> do
      mainLib <- mainLibExposed l
      subLibs <- subLibsExposed (libSrcDir l)
      pure (AllExposed mainLib subLibs)
  where
    ppExts :: [String]
    ppExts = nub (customPPExts ++ map fst knownSuffixHandlers)
    libSrcDir :: Library -> [FilePath]
    libSrcDir = nub . hsSourceDirs . libBuildInfo
    mainLibExcludedPaths :: [[FilePath]]
    mainLibExcludedPaths =
      (map snd subLibSrcDirs) ++ (nonLibraryHsSourcePaths pds)
    mainLibExposed :: Library -> IO ExposedLib
    mainLibExposed l = do
      exposedMods <- getExposedModules (sourceExtensions ++ ppExts) (libSrcDir l) mainLibExcludedPaths
      exposedSigs <- getExposedModules hsigExtensions (libSrcDir l) mainLibExcludedPaths
      pure (ExposedLib exposedMods exposedSigs)
    subLibSrcDirs :: [(Library,[FilePath])]
    subLibSrcDirs = zip (packageDescriptionSubsetSubLibraries pds) (map libSrcDir (packageDescriptionSubsetSubLibraries pds))
    subLibSrcDirsWithExcludedPaths :: [FilePath] -> [(Library, ([FilePath], [[FilePath]]))]
    subLibSrcDirsWithExcludedPaths mainLibSrcs =
      map (\((subLib,subLibSrcs),otherSubLibs) ->
             let excluded = (map snd otherSubLibs) ++ (nonLibraryHsSourcePaths pds) ++ [mainLibSrcs]
             in (subLib, (subLibSrcs,excluded))
          )
          (indexWithNeighbors subLibSrcDirs)
    subLibsExposed :: [FilePath] -> IO [(Library,ExposedLib)]
    subLibsExposed mainLibSrcs =
      mapM (\(subLib,(subLibSrcs,excluded)) -> do
               exposedMods <- getExposedModules (sourceExtensions ++ ppExts) subLibSrcs excluded
               exposedSigs <- getExposedModules hsigExtensions subLibSrcs excluded
               pure (subLib, ExposedLib exposedMods exposedSigs)
           )
           (subLibSrcDirsWithExcludedPaths mainLibSrcs)

-- | Since the @hs-source-dirs@ fields in a @.cabal@ file take a source tree
-- path relative to the @.cabal@ file itself we need to make sure the current
-- working directory in which to search for module detection is the directory in
-- which the @.cabal@ file resides.
withCabalFileDirectoryCwd
  :: HasCallStack
  => Maybe FilePath -- ^ Absolute path to the directory containing a '.cabal' file, current directory if absent
  -> IO a -- ^ The IO action that auto detects modules & signatures
  -> IO a
withCabalFileDirectoryCwd projectPathM action = do
  let pp = fromMaybe "." projectPathM
  cabalFilePath <- findPackageDesc pp
  case cabalFilePath of
    Left err -> error err
    Right _ -> withCurrentDirectory pp action

-- | Update the exposed modules and signatures of a 'Library'
updateLibrary :: Library -> ExposedLib -> Library
updateLibrary lib exposedLib =
  lib & (L.exposedModules %~ (nub . (++) (exposedLibModules exposedLib)))
      . (L.signatures %~ (nub . (++) (exposedLibSignatures exposedLib)))

-- | Update the 'PackageDescription' of this package to include auto detected
-- library modules. Also just to be nice fill in the 'Paths_...' module in
-- 'otherModules' field of the library's 'BuildInfo'.
updatePackageDescription :: HasCallStack => PackageDescription -> UserHooks -> IO PackageDescription
updatePackageDescription pd uhs =
  withCabalFileDirectoryCwd Nothing $ do
    (AllExposed exposedLib exposedSubLibs) <-
      getAllExposed (packageDescriptionToSubset pd) (map fst (hookedPreProcessors uhs))
    let newMainLibrary l =
          (updateLibrary l exposedLib) &
            (L.libBuildInfo . L.otherModules %~ (nub . (++) [(autogenPathsModuleName pd)]))
    pure (pd { library = fmap newMainLibrary (library pd)
             , subLibraries = map (uncurry updateLibrary) exposedSubLibs
             })

-- | Update the 'GenericPackageDescription' of this package so the library can
-- be properly instantiated with Backpack signatures at configure time when the
-- 'confHook' is run.
updateGenericPackageDescription ::
  HasCallStack
  => Maybe FilePath -- ^ Absolute path of the directory which contains the '.cabal' file, in other words the root of the project
  -> GenericPackageDescription
  -> [String] -- ^ Custom preprocessor extensions
  -> IO GenericPackageDescription
updateGenericPackageDescription projectPath gpd ppExts =
  let updateCondTreeLib exposedLib condLib =
        condLib { condTreeData = updateLibrary (condTreeData condLib) exposedLib  }
  in withCabalFileDirectoryCwd projectPath $ do
      (AllExposed exposedLib exposedSubLibs) <-
        getAllExposed (genericPackageDescriptionToSubset gpd) ppExts
      pure $
        gpd { condLibrary = fmap (updateCondTreeLib exposedLib) (condLibrary gpd)
            , condSubLibraries =
                map (\((unqualName,condSubLib),subLib) -> (unqualName,condSubLib { condTreeData = subLib }))
                    (zip (condSubLibraries gpd) (map (uncurry updateLibrary) exposedSubLibs))
            }

-- | The default name to use when generating an explicit Cabal file
-- It defaults to @<package-name>-<package-version>-generated.cabal@
defaultGeneratedCabalName  :: GenericPackageDescription -> FilePath
defaultGeneratedCabalName gpd =
  let gpdPkg = package . packageDescription
  in (prettyShow (pkgName (gpdPkg gpd)))
     ++ "-"
     ++ (prettyShow (pkgVersion (gpdPkg gpd)))
     ++ "-generated"
     <.> "cabal"

autoExposeConfHook
  :: UserHooks
  -> Maybe WriteGeneratedCabal
  -> (GenericPackageDescription, HookedBuildInfo)
  -> ConfigFlags
  -> IO LocalBuildInfo
autoExposeConfHook userHooks writeGeneratedCabalM (gpd,hbi) cfs = do
  newGpd <- updateGenericPackageDescription (Just ".") gpd (map fst (hookedPreProcessors userHooks))
  whenJust writeGeneratedCabalM
    (\(WriteGeneratedCabal outputDir generatedCabalName) -> do
        let f = outputDir </> (generatedCabalName newGpd)
        notice (fromFlag (configVerbosity cfs)) ("Writing generated Cabal file: " ++ f)
        writeGenericPackageDescription f newGpd
    )
  (confHook userHooks) (newGpd,hbi) cfs

-- | Modify a set of 'UserHooks' so that all relevant hooks see a
-- 'PackageDescription' or 'GenericPackageDescription' with auto detected
-- modules and signatures filled in.
--
-- Also optionally write an explicit Cabal file at 'confHook' time.
autoExposeHooks :: Maybe WriteGeneratedCabal -> UserHooks -> UserHooks
autoExposeHooks writeGeneratedCabalM userHooks =
  userHooks
  { confHook = autoExposeConfHook userHooks writeGeneratedCabalM
  , buildHook = bh
  , replHook = rh
  , hscolourHook = hscolourH
  , doctestHook = dth
  , haddockHook = hh
  , copyHook = ch
  , instHook = ih
  , testHook = th
  , benchHook = benchH
  , regHook = regH
  , unregHook = unregH
  }
  where
    bh :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
    bh pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (buildHook userHooks) newPd lbi uhs fs
    rh :: PackageDescription -> LocalBuildInfo -> UserHooks -> ReplFlags -> [String] -> IO ()
    rh pd lbi uhs fs opts = do
      newPd <- updatePackageDescription pd uhs
      (replHook userHooks) newPd lbi uhs fs opts
    hscolourH :: PackageDescription -> LocalBuildInfo -> UserHooks -> HscolourFlags -> IO ()
    hscolourH pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (hscolourHook userHooks) newPd lbi uhs fs
    dth :: PackageDescription -> LocalBuildInfo -> UserHooks -> DoctestFlags -> IO ()
    dth pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (doctestHook userHooks) newPd lbi uhs fs
    hh :: PackageDescription -> LocalBuildInfo -> UserHooks -> HaddockFlags -> IO ()
    hh pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (haddockHook userHooks) newPd lbi uhs fs
    ch :: PackageDescription -> LocalBuildInfo -> UserHooks -> CopyFlags -> IO ()
    ch pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (copyHook userHooks) newPd lbi uhs fs
    ih :: PackageDescription -> LocalBuildInfo -> UserHooks -> InstallFlags -> IO ()
    ih pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (instHook userHooks) newPd lbi uhs fs
    th :: Args -> PackageDescription -> LocalBuildInfo -> UserHooks -> TestFlags -> IO ()
    th args pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (testHook userHooks) args newPd lbi uhs fs
    benchH :: Args -> PackageDescription -> LocalBuildInfo -> UserHooks -> BenchmarkFlags -> IO ()
    benchH args pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (benchHook userHooks) args newPd lbi uhs fs
    regH :: PackageDescription -> LocalBuildInfo -> UserHooks -> RegisterFlags -> IO ()
    regH pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (regHook userHooks) newPd lbi uhs fs
    unregH :: PackageDescription -> LocalBuildInfo -> UserHooks -> RegisterFlags -> IO ()
    unregH pd lbi uhs fs = do
      newPd <- updatePackageDescription pd uhs
      (unregHook userHooks) newPd lbi uhs fs

-- | The supported Haskell source extensions, currently 'hs' and 'lhs'
sourceExtensions :: [String]
sourceExtensions = ["hs","lhs"]

-- | Backpack signature extensions, currently 'hsig' and 'lhsig'
hsigExtensions :: [String]
hsigExtensions = ["hsig","lhsig"]