cartel-0.2.0.0: lib/Cartel/Tools.hs
module Cartel.Tools
( -- * Building constraints
-- | Helpers to build many common version constraints.
-- ** Building constraint trees
(&&&)
, (|||)
, lt
, gt
, eq
, ltEq
, gtEq
-- ** Building package specifications
, closedOpen
, apiVersion
, nextBreaking
, nextMajor
, exactly
-- * Rendering a cabal file
, render
, renderString
-- * Getting a list of all modules in a directory tree
, modules
, fileExtensions
, modulesWithExtensions
) where
import qualified Cartel.Ast as A
import qualified Cartel.Render as R
import qualified System.Directory as D
import qualified System.FilePath as P
import System.FilePath ((</>))
import qualified Data.Char as C
import Data.List (intersperse, sortBy)
import Data.Monoid
import qualified Data.Version as V
import Data.Time
import qualified Paths_cartel
import qualified System.IO as IO
(&&&) :: A.ConstrTree -> A.ConstrTree -> A.ConstrTree
l &&& r = A.Branch A.And l r
infixr 3 &&&
(|||) :: A.ConstrTree -> A.ConstrTree -> A.ConstrTree
l ||| r = A.Branch A.Or l r
infixr 2 |||
lt :: [Int] -> A.ConstrTree
lt = A.Leaf . A.Constraint LT . A.Version
gt :: [Int] -> A.ConstrTree
gt = A.Leaf . A.Constraint GT . A.Version
eq :: [Int] -> A.ConstrTree
eq = A.Leaf . A.Constraint EQ . A.Version
ltEq :: [Int] -> A.ConstrTree
ltEq v = lt v ||| eq v
gtEq :: [Int] -> A.ConstrTree
gtEq v = gt v ||| eq v
-- | Creates a package interval that is closed on the left, open on
-- the right. Useful for the common case under the PVP to specify
-- that you depend on a version that is at least a particular
-- version, but less than another version.
--
-- > closedOpen "bytestring" [0,17] [0,19] ==> bytestring >= 0.17 && < 0.19
closedOpen
:: String
-- ^ Package name
-> [Int]
-- ^ Version number for lower bound
-> [Int]
-- ^ Version number for upper bound
-> A.Package
-- ^ Resulting contraints
closedOpen n l u = A.Package n $ Just (gtEq l &&& lt u)
-- | Specifies a particular API version. Useful to lock your
-- package dependencies down to a particular API version.
--
-- > apiVersion "base" [1] ==> base >= 1 && < 2
-- > apiVersion "base" [1,2] ==> base >= 1.2 && < 1.3
-- > apiVersion "base" [1,2,3] ==> base >= 1.2.3 && < 1.2.4
apiVersion :: String -> [Int] -> A.Package
apiVersion n v = closedOpen n v u
where
u = case v of
[] -> error "apiVersion: requires a non-empty list argument"
_ -> init v ++ [succ (last v)]
-- | Depends on the version given, up to the next breaking API
-- change.
--
-- > nextBreaking "base" [4] ==> base >= 4 && < 4.1
-- > nextBreaking "base" [4,1] ==> base >= 4.1 && < 4.2
-- > nextBreaking "base" [4,7,0,0] ==> base >= 4.7.0.0 && < 4.8
nextBreaking
:: String
-> [Int]
-> A.Package
nextBreaking n v = closedOpen n v u
where
u = case v of
[] -> error "nextBreaking: requires a non-empty list argument"
x:[] -> [x, 1]
x:y:_ -> x : succ y : []
-- | Depends on the version given, up to the next time the first
-- digit increments. Useful for @base@.
--
-- > nextBreaking "base" [4] ==> base >= 4 && < 5
nextMajor
:: String
-> [Int]
-> A.Package
nextMajor n v = closedOpen n v u
where
u = case v of
[] -> error "nextMajor: requires a non-empty list argument"
x:_ -> succ x : []
-- | Depends on exactly this version only.
--
-- > exactly "base" [4,5,0,0] ==> base ==4.5.0.0
exactly :: String -> [Int] -> A.Package
exactly n v = A.Package n (Just $ eq v)
-- | Common extensions of Haskell files and files that are
-- preprocessed into Haskell files. Includes:
--
-- * hs (Haskell)
-- * lhs (literate Haskell)
-- * gc (greencard)
-- * chs (c2hs)
-- * hsc (hsc2hs)
-- * y and ly (happy)
-- * x (alex)
-- * cpphs
fileExtensions :: [String]
fileExtensions =
[ "hs"
, "lhs"
, "gc"
, "chs"
, "hsc"
, "y"
, "ly"
, "x"
, "cpphs"
]
interestingFile
:: [String]
-- ^ Extensions of module files
-> FilePath
-> Bool
interestingFile xs s = case s of
"" -> False
x:_
| not (C.isUpper x) -> False
| otherwise -> let mayExt = P.takeExtension s
in case mayExt of
[] -> False
_ : ext -> ext `elem` xs
interestingDir :: FilePath -> Bool
interestingDir p = case p of
[] -> False
x:_
| not (C.isUpper x) -> False
| otherwise -> not $ '.' `elem` p
-- | Gets all Haskell modules in a given directory tree. Only files
-- with one of the extensions listed in 'fileExtensions' are
-- returned. Files and directories that do not begin with an
-- uppercase letter are ignored. (This also ignores files that
-- start with a dot.) Directories with a dot anywhere in the name
-- are ignored.
modules
:: FilePath
-- ^ Start searching within this directory.
-> IO [String]
-- ^ A list of Haskell modules in the given directory tree. The
-- file contents are not examined; only the file names matter.
-- Returned as a list of dotted names.
modules = modulesWithExtensions fileExtensions
-- | Gets all Haskell modules in a given directory tree. Allows you
-- to specify what extensions you are interested in.
modulesWithExtensions
:: [String]
-- ^ Look for files that have one of these extensions.
-- 'fileExtensions' covers the most common cases. Files without
-- one of these extensions are ignored. Files and directories
-- that do not begin with an uppercase letter are ignored. (This
-- also ignores files that start with a dot.) Directories with a
-- dot anywhere in the name are ignored.
-> FilePath
-- ^ Start searching within this directory.
-> IO [String]
-- ^ A list of Haskell modules in the given directory tree. The
-- file contents are not examined; only the file names matter.
-- Returned as a list of dotted names.
modulesWithExtensions exts start
= fmap (map modName . sortBy sorter . map reverse)
$ modulesInDir exts start []
where
modName = concat . intersperse "."
sorter :: [String] -> [String] -> Ordering
sorter x y = mconcat (zipWith compare x y) <> compare lenX lenY
where
(lenX, lenY) = (length x, length y)
modulesInDir
:: [String]
-- ^ Extensions of module files
-> FilePath
-- Search is rooted in this directory
-> [FilePath]
-- ^ Stack of directories we're in
-> IO [[String]]
-- ^ Returns a list of modules in this directory.
modulesInDir exts start dirs = do
cs <- D.getDirectoryContents (start </> P.joinPath (reverse dirs))
fmap concat . mapM (processFile exts start dirs) $ cs
processFile
:: [String]
-- ^ Extensions of module files
-> FilePath
-- ^ Search is rooted in this directory
-> [FilePath]
-- Stack of directories we're in, including current directory
-> FilePath
-- ^ Interesting file under investigation
-> IO [[String]]
processFile exts start dirs this = do
isDir <- D.doesDirectoryExist
(start </> (P.joinPath . reverse $ this : dirs))
if isDir
then if interestingDir this
then modulesInDir exts start (this : dirs)
else return []
else return $ if interestingFile exts this
then [(P.dropExtension this : dirs)]
else []
-- | Renders a 'A.Cabal' data type as a string.
renderString
:: String
-- ^ Name of program used
-> ZonedTime
-- ^ When this output is being created
-> V.Version
-- ^ Cartel package version
-> A.Cabal
-> String
renderString nm zt ver cbl = hdr ++ R.cabal cbl
where
hdr = unlines $
[ "-- This Cabal file generated using the Cartel library."
, "-- Cartel is available at:"
, "-- http://www.github.com/massysett/cartel"
, "--"
] ++ case nm of
[] -> []
_ -> ["-- Script name used to generate: " ++ nm]
++
[ "-- Generated on: " ++ show zt
, "-- Cartel library version: " ++ showVer
]
showVer = concat
. intersperse "."
. map show
. V.versionBranch
$ ver
-- | Render a Cabal file to standard output. The output will have
-- comments at the beginning indicating that it was built with
-- Cartel, and what version of the Cartel library was used, and when
-- the output was produced, along with (optionally) the filename of
-- the program used to produce the output.
--
-- Ensures that the output is UTF-8, as required by Cabal.
render
:: String
-- ^ The name of the program used to produce this output. Put the
-- name of your script here so it can appear in the output. This
-- is optional; to omit it, use the empty string here.
-> A.Cabal
-> IO ()
render nm cbl = do
zt <- getZonedTime
IO.hSetBinaryMode IO.stdout False
IO.hSetEncoding IO.stdout IO.utf8
putStr $ renderString nm zt Paths_cartel.version cbl