Safe Haskell | None |
---|---|
Language | Haskell2010 |
Velma is a Haskell package that makes it easy to automatically add files
to exposed-modules
and other-modules
in Cabal package descriptions.
Motivation
When working on a Haskell application, it can get tedious to update the
package description (*.cabal
file) every time you add, rename, or remove a
module. What's worse is that Cabal can clearly figure this out on its own
since it warns you about it!
<no location info>: warning: [-Wmissing-home-modules] These modules are needed for compilation but not listed in your .cabal file's other-modules: Velma.SymbolicPath
So what gives? The package description is in an unfortunate situation: It's meant to be a human writable file, but it's also meant to be machine readable. When someone uploads a package to Hackage, it's important that all of that package's modules be known statically.
But many Haskell projects are never going to be uploaded to Hackage, and the list of exposed modules is essentially just every Haskell file in some directory. That's the problem that Velma aims to solve.
Usage
Velma is implemented as a custom setup script. Read more about them here: https://cabal.readthedocs.io/en/3.6/cabal-package.html#custom-setup-scripts.
To use Velma, you need to do a few things:
Change your build type from
Simple
toCustom
:-- *.cabal build-type: Custom
If your
*.cabal
file does not already have abuild-type
field then just add it and set it toCustom
.If you're already using a custom setup script, then you probably know what you're doing. You'll probably want to integrate Velma's
confHook
into your custom setup.Add a
custom-setup
stanza:-- *.cabal custom-setup setup-depends: base, Cabal, velma
If you're using
cabal-install
then you can remove theCabal
dependency. For some reason Stack requires it.Add
Velma.Discover
to yourexposed-modules
orother-modules
:-- *.cabal library exposed-modules: Velma.Discover
Velma will only discover modules in places where
Velma.Discover
is present. That means you can explicitly list yourexposed-modules
but let Velma discover yourother-modules
. Or you can use Velma only for your test suite. It's up to you!Create a
Setup.hs
file:-- Setup.hs import Velma main = defaultMain
Limitations
- Only
*.hs
files are discovered.
- All conditionals are ignored.
- The
cabal sdist
command will not automatically discover modules. This will likely lead to an error such as this: "Setup.hs: Error: Could not find module: Velma.Discover with any suffix: [...]. If the module is autogenerated it should be added to 'autogen-modules'." https://github.com/haskell/cabal/issues/3424 - The
stack build
command will generate warnings about missing modules. This warning is safe to ignore. Unfortunately it's visually noisy and there's no way to disable it. https://github.com/commercialhaskell/stack/issues/1881
Synopsis
- defaultMain :: IO ()
- userHooks :: UserHooks
- confHook :: (GenericPackageDescription, HookedBuildInfo) -> ConfigFlags -> IO LocalBuildInfo
- discover :: GenericPackageDescription -> IO GenericPackageDescription
- discoverWith :: Monad m => (FilePath -> m [FilePath]) -> GenericPackageDescription -> m GenericPackageDescription
- discoverLibrary :: Monad m => (FilePath -> m [FilePath]) -> Library -> m Library
- discoverForeignLib :: Applicative m => (FilePath -> m [FilePath]) -> ForeignLib -> m ForeignLib
- discoverExecutable :: Applicative m => (FilePath -> m [FilePath]) -> Executable -> m Executable
- discoverTestSuite :: Applicative m => (FilePath -> m [FilePath]) -> TestSuite -> m TestSuite
- discoverBenchmark :: Applicative m => (FilePath -> m [FilePath]) -> Benchmark -> m Benchmark
- discoverComponent :: (HasBuildInfo a, Applicative m) => Lens' a [ModuleName] -> (a -> [ModuleName]) -> (FilePath -> m [FilePath]) -> a -> m a
- getModuleNames :: (HasBuildInfo a, Applicative m) => (FilePath -> m [FilePath]) -> a -> m (Set ModuleName)
- getHsSourceDirs :: HasBuildInfo a => a -> [FilePath]
- filePathToModuleName :: FilePath -> Maybe ModuleName
- listDirectoryRecursively :: FilePath -> IO [FilePath]
- concatM :: Monad m => [a -> m a] -> a -> m a
- condTreeData :: Lens' (CondTree v c a) a
- maybeRemove :: Eq a => a -> [a] -> Maybe [a]
- overF :: Functor f => Lens' s a -> (a -> f a) -> s -> f s
- withDefault :: Foldable t => t a -> t a -> t a
Documentation
defaultMain :: IO () Source #
The default entrypoint for this custom setup script. This calls Cabal's
defaultMainWithHooks
with our custom userHooks
.
If you're trying to use Velma in your own project, you should create a
Setup.hs
file like this:
-- Setup.hs import Velma main = defaultMain
confHook :: (GenericPackageDescription, HookedBuildInfo) -> ConfigFlags -> IO LocalBuildInfo Source #
Calls discover
before handing things off to the confHook
from
Cabal's simpleUserHooks
.
discover :: GenericPackageDescription -> IO GenericPackageDescription Source #
Simply calls discoverWith
with listDirectoryRecursively
.
:: Monad m | |
=> (FilePath -> m [FilePath]) | |
-> GenericPackageDescription | |
-> m GenericPackageDescription |
Discovers modules in all of the components of this package description.
You can think of this function as calling discoverComponent
for each
component: library, sub-libraries, foreign libraries, executables, test
suites, and benchmarks.
Thin wrapper around discoverComponent
for libraries.
:: Applicative m | |
=> (FilePath -> m [FilePath]) | |
-> ForeignLib | |
-> m ForeignLib |
Thin wrapper around discoverComponent
for foreign libraries.
:: Applicative m | |
=> (FilePath -> m [FilePath]) | |
-> Executable | |
-> m Executable |
Thin wrapper around discoverComponent
for executables.
:: Applicative m | |
=> (FilePath -> m [FilePath]) | |
-> TestSuite | |
-> m TestSuite |
Thin wrapper around discoverComponent
for test suites.
:: Applicative m | |
=> (FilePath -> m [FilePath]) | |
-> Benchmark | |
-> m Benchmark |
Thin wrapper around discoverComponent
for benchmarks.
:: (HasBuildInfo a, Applicative m) | |
=> Lens' a [ModuleName] | Typically something like |
-> (a -> [ModuleName]) | This function is used to get a list of module names to avoid
discovering. For example if you're populating |
-> (FilePath -> m [FilePath]) | |
-> a | |
-> m a |
Discovers modules in the given component, using the provided lens to select which field to update. This is the main workhorse of the package.
:: (HasBuildInfo a, Applicative m) | |
=> (FilePath -> m [FilePath]) | |
-> a | |
-> m (Set ModuleName) |
Gets module names for the given component, using the provided function to
list directory contents. This basically just glues together
getHsSourceDirs
, listDirectoryRecursively
, and filePathToModuleName
.
getHsSourceDirs :: HasBuildInfo a => a -> [FilePath] Source #
Gets hs-source-dirs
from the given component.
- If
hs-source-dirs
isn't set (or is empty), this will return the inferred directory, which is the current directory ("."
). - Duplicates are removed from the result using
nubOrd
. - This should probably return
SymbolicPath
values, but that type was only introduced in recent versions (>= 3.6) of Cabal.
filePathToModuleName :: FilePath -> Maybe ModuleName Source #
Attempts to convert a FilePath
into a ModuleName
. This
works by stripping certain extensions, then converting directory separators
into module separators, and finally trying to parse that as a module name.
>>>
filePathToModuleName "Velma.hs"
Just (ModuleName "Velma")>>>
filePathToModuleName "Velma/SymbolicPath.hs"
Just (ModuleName "Velma.SymbolicPath")>>>
filePathToModuleName "README.markdown"
Nothing>>>
filePathToModuleName "library/Velma.hs"
Nothing
listDirectoryRecursively :: FilePath -> IO [FilePath] Source #
Lists all of the directory contents recursively. The returned file paths
will include the directory prefix, unlike listDirectory
. For
example:
>>>
listDirectoryRecursively "source/library"
["source/library/Velma.hs","source/library/Velma/SymbolicPath.hs"]
concatM :: Monad m => [a -> m a] -> a -> m a Source #
Applies all of the functions left-to-right using (>=>)
.
>>>
let printAnd f x = do { putStrLn $ "x = " <> show x; pure $ f x }
>>>
concatM [ printAnd (+ 2), printAnd (* 2) ] 3
x = 3 x = 5 10
condTreeData :: Lens' (CondTree v c a) a Source #
A lens for the condTreeData
field.
maybeRemove :: Eq a => a -> [a] -> Maybe [a] Source #
Attempts to remove an element from the list. If it succeeds, returns the
list without that element. If it fails, returns Nothing
.
>>>
maybeRemove 'b' "abc"
Just "ac">>>
maybeRemove 'z' "abc"
Nothing
Note that only the first matching element is removed.
>>>
maybeRemove 'b' "abcb"
Just "acb"
overF :: Functor f => Lens' s a -> (a -> f a) -> s -> f s Source #
Like over
except the modification function can perform arbitrary
effects.
>>>
overF _2 (Just . (+ 2)) ('a', 3)
Just ('a',5)>>>
overF _2 (const Nothing) ('a', 3)
Nothing