velma-0.2022.2.13: Automatically add files to exposed-modules and other-modules.
Safe HaskellNone
LanguageHaskell2010

Velma

Description

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 to Custom:

    -- *.cabal
    
    build-type: Custom

    If your *.cabal file does not already have a build-type field then just add it and set it to Custom.

    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 the Cabal dependency. For some reason Stack requires it.

  • Add Velma.Discover to your exposed-modules or other-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 your exposed-modules but let Velma discover your other-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

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

userHooks :: UserHooks Source #

Like Cabal's simpleUserHooks but with our custom confHook.

discoverWith Source #

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.

discoverLibrary Source #

Arguments

:: Monad m 
=> (FilePath -> m [FilePath])

See listDirectoryRecursively.

-> Library 
-> m Library 

Thin wrapper around discoverComponent for libraries.

discoverForeignLib Source #

Thin wrapper around discoverComponent for foreign libraries.

discoverExecutable Source #

Thin wrapper around discoverComponent for executables.

discoverTestSuite Source #

Arguments

:: Applicative m 
=> (FilePath -> m [FilePath])

See listDirectoryRecursively.

-> TestSuite 
-> m TestSuite 

Thin wrapper around discoverComponent for test suites.

discoverBenchmark Source #

Arguments

:: Applicative m 
=> (FilePath -> m [FilePath])

See listDirectoryRecursively.

-> Benchmark 
-> m Benchmark 

Thin wrapper around discoverComponent for benchmarks.

discoverComponent Source #

Arguments

:: (HasBuildInfo a, Applicative m) 
=> Lens' a [ModuleName]

Typically something like exposedModules.

-> (a -> [ModuleName])

This function is used to get a list of module names to avoid discovering. For example if you're populating exposedModules, then you'll want to use otherModules here to avoid discovering duplicates.

-> (FilePath -> m [FilePath])

See listDirectoryRecursively.

-> 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.

getModuleNames Source #

Arguments

:: (HasBuildInfo a, Applicative m) 
=> (FilePath -> m [FilePath])

See listDirectoryRecursively.

-> 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

withDefault Source #

Arguments

:: Foldable t 
=> t a

The default value.

-> t a 
-> t a 

Returns the given default value if the other value is null. For example:

>>> withDefault ["default"] []
["default"]
>>> withDefault ["default"] ["something"]
["something"]