Multiple GHC versions
You might have multiple stack.yaml
files for different GHC versions. ghcide and haskell-language-server do this with corresponding stack-8.x.y.yaml
files that allow them to be built with a myriad of different GHCs, by using specific snapshots.
If you're using Cabal though, you manage your GHC installations yourself. Your installed ghcs (and ghc-pkgs) probably have their versions suffixed to them, where ghc
is just a symlink to a specific version of choice.
$ ls -1 /usr/local/bin/ghc*
/usr/local/bin/ghc
/usr/local/bin/ghc-8.10.1
/usr/local/bin/ghc-8.6.5
/usr/local/bin/ghc-8.8.3
/usr/local/bin/ghc-pkg
/usr/local/bin/ghc-pkg-8.10.1
/usr/local/bin/ghc-pkg-8.6.5
/usr/local/bin/ghc-pkg-8.8.3
You can tell Cabal to build your project with a specific GHC1 with the --with-compiler
flag, or -w
for short:
$ cabal build -wghc-8.8.3
If you want Cabal to remember this so you don't need to pass the flag every time, put it in your cabal.project
packages: .
with-compiler: ghc-8.8.3
"But my project is in source control and built by other people. How can I get Cabal to use a specific GHC version without forcing everyone else to build it with the same GHC version?", you ask.
That's exactly what cabal.project.local
is for.
It should be added to your .gitignore
, and is intended for your individual
local changes.
Either make one by hand, or use cabal configure
to generate one.
$ cabal configure -wghc-8.8.3
...
$ cat cabal.project.local
with-compiler: ghc-8.8.3
Snapshots
Snapshots are Stack's flagship feature which ensures that all packages are
buildable with each other at any given time. You can kind of get the same thing in Cabal with --index-state
:
$ cabal configure --index-state=2019-11-24T17:23:36Z
...
$ cat cabal.project.local
index-state: 2019-11-24T17:23:36Z
If you ever find yourself reminiscing about the good old days when your package constraints were solvable, put one of these into your cabal.project[.local]
and Cabal will use the index of Hackage at that point in time. Or the closest one available, if there's no exact match for it. It's not
exactly the same thing as a Stack snapshot, since not all packages are guaranteed to build with each
other. But you won't be surprised with sudden breakages whenever a dependency
has a new version published.
If you already had a cabal.project.local
when you ran cabal.configure
, you might have also
noticed there's now a cabal.project.local~
: It's a backup of the old one before you configured, just in case.
Freezing
You can go a step further and instead of persisting the state of Hackage, you can persist the exact versions of each package that Cabal's solver picked out.
$ cabal freeze
Wrote freeze file: /Users/luke/foo/cabal.project.freeze
The freeze file is actually just another cabal.project
, but with the
version of every package locked into place in a list of constraints. Feel free to check this into source control. It's the Cabal equivalent of a package-lock.json
$ cat cabal.project.freeze
constraints: any.Cabal ==3.0.1.0,
any.QuickCheck ==2.14,
QuickCheck +templatehaskell,
any.StateVar ==1.2,
any.aeson ==1.4.7.1,
...
jlombera pointed out that you can in fact, pretty much replicate Stackage in Cabal by freezing with a specific Stackage LTS, by downloading a config file provided:
curl https://www.stackage.org/lts-15.15/cabal.config > cabal.project.freeze
Local repositories
You've probably been pulling in all your packages from Hackage, which is the
default package repository.
However you can roll your own if
you need a place to store your private packages. If you have the URL to it, you
can tell cabal to search for packages in it by editing your ~/.cabal/config
:
repository hackage.haskell.org
url: http://hackage.haskell.org/
repository luxurious-private-repo
url: http://pkgs.lukelau.me/
If you don't need to share your packages with anyone else, you can use a local folder of source distributions (sdists) as a repository. First create your sdists2
$ cd ~/foo
$ tar -czf foo-0.1.0.0.tar.gz !(dist-newstyle)
$ cd ~/bar # or if you have a git repository
$ git archive HEAD -o foo-0.1.0.0.tar.gz
$ cd ~/baz # or like a normal person
$ cabal sdist
Wrote tarball sdist to
/Users/luke/baz/dist-newstyle/sdist/baz-0.1.0.0.tar.gz
Then place them into a folder
$ ls ~/local-repo
bar-0.1.0.0.tar.gz foo-0.1.0.0.tar.gz
Then add the repository to your config file
repository my-local-repository
url: file+noindex:///Users/luke/local-repo
Now you can start pulling them in as dependencies. Unlike adding a folder to the cabal.project
packages field, this acts as a repository so you can store multiple sdists of a package with different
versions.
Source repository packages
Often times if you're waiting for a dependency to be fixed upstream, you might
find yourself adding a git submodule to tide yourself over until a new version is uploaded to Hackage.
Cabal can help you avoid submodules by pulling in packages from remote version control systems. Just specify them inside your cabal.project
:
source-repository-package
type: git
location: https://github.com/haskell/haskell-ide-engine.git
branch: quick-fix
subdir: hie-plugin-api
It supports quite a few version control systems: Mercurial, Darcs and Bazaar just to name a few.
Vendoring
Thanks to Faucelme for mentioning that there are many other ways to vendor packages – that is tell Cabal to
use a local version of a package in place of the version on Hackage. You can simply add the package
to your cabal.project
. A quick way to make changes is to use cabal get
to fetch and unpack a
package you want to tweak:
$ cabal get aeson
Unpacking to aeson-1.5.1.0/
$ cat cabal.project
packages:
. -- the package in the current directory
../aeson-1.5.1.0 -- the local copy of aeson
Or if the tweaked package is hosted somewhere, you can even specify a HTTP URL!
Scripts
runghc
/runhaskell
is great for scripting in a pinch.
#!/usr/bin/env runghc
main = putStrLn "hey"
$ chmod u+x Script.hs
$ ./Script.hs
"hey"
But the fun pretty much stops as soon as you need to use an external package.
#!/usr/bin/env runghc
{-# LANGUAGE TypeApplications #-}
import Data.Aeson -- this isn't in the core libraries!
import Data.Text.Lazy (pack)
import Data.Text.Lazy.Encoding (encodeUtf8)
main = getLine >>= print . decode @[Int] . encodeUtf8 . pack
$ echo [1,2,3] | ./Script.hs
Script.hs:3:1: error:
Could not find module ‘Data.Aeson’
Perhaps you meant Data.Version (from base-4.14.0.0)
Use -v (or `:set -v` in ghci) to see a list of the files searched for.
|
3 | import Data.Aeson
| ^^^^^^^^^^^^^^^^^
You could try installing the library locally and then running it:
$ cabal install aeson --lib
$ echo [1,2,3] | ./Script.hs
Just [1,2,3]
But if you want to then share the script others will also have to make sure aeson
is installed locally.
And where does the installed library even end up? And how do you uninstall it?
It gets installed into the Cabal store and then registered in Cabal's GHC package database, under
the name shortened name sn
~/.cabal/store/ghc-8.10.1/sn-1.5.1.0-ec3f28f3/
~/.cabal/store/ghc-8.10.1/package.db/sn-1.5.1.0-ec3f28f3.conf
and then exposed through the default GHC environment
$ cat ~/.ghc/x86_64-darwin-8.10.1/environments/default
clear-package-db
global-package-db
package-db /Users/luke/.cabal/store/ghc-8.10.1/package.db
package-id ghc-8.10.1
package-id bytestring-0.10.10.0
...
package-id sn-1.5.1.0-ec3f28f3
If you want to make your script portable, avoid cluttering your environment and if this is all just a bit over your head, then you can just turn it into a Cabal script:
#!/usr/bin/env cabal
{- cabal:
build-depends: base, text, aeson ^>= 1.5
-}
{-# LANGUAGE TypeApplications #-}
import Data.Aeson
import Data.Text.Lazy (pack)
import Data.Text.Lazy.Encoding (encodeUtf8)
main = getLine >>= print . decode @[Int] . encodeUtf8 . pack
Just put your dependencies at the top and Cabal will run it through a miniature package.
$ echo [1,2,3] | ./Script.hs
Resolving dependencies...
Build profile: -w ghc-8.10.1 -O1
In order, the following will be built (use -v for more details):
- fake-package-0 (exe:script) (first run)
Configuring executable 'script' for fake-package-0..
Preprocessing executable 'script' for fake-package-0..
Building executable 'script' for fake-package-0..
...
Just [1,2,3]
Haddocks
It's often handy to see the haddocks of the project you're working on.
No need to wait before you upload it Hackage though. You can build it locally with cabal haddock
$ cabal haddock
Build profile: -w ghc-8.10.1 -O1
In order, the following will be built (use -v for more details):
- foo-0.1.0.0 (lib) (configuration changed)
Configuring library for foo-0.1.0.0..
Preprocessing library for foo-0.1.0.0..
Running Haddock on library for foo-0.1.0.0..
Haddock coverage:
100% ( 2 / 2) in 'MyLib'
Documentation created:
/Users/luke/foo/dist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/index.html
#Â browse to your hearts content
$ open /Users/luke/foo/dist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/index.html
Open up the index.html
in your browser of choice and you're good to go. It also shows the output of
running haddock
so you can see where you've missed any documentation.
If you're like me you might often take advantage of the quickjump functionality in Haddock. You can
press the 's' key when browsing a Haddock package to bring up a search box, from
which you can jump to definitions throughout. It's enabled on the haddock built on Hackage, and you
can get it locally too with the --haddock-quickjump
flag. Just be aware that you'll need to properly
serve the pages with a HTTP server to get around the same-origin security restrictions.
$ cabal haddock --haddock-quickjump
# if you have python installed, this is a quick way to serve pages
$ python3 -mhttp.server -ddist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/
Hoogle
Why not take the search experience to the next level by generating a hoogle database with --haddock-hoogle
?
$ cabal haddock --haddock-hoogle
...
Documentation created:
/Users/luke/foo/dist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/foo.txt
$ hoogle generate --local=dist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/
Starting generate
[1/1] foo... 0.00s
Reordering items... 0.00s
Writing tags... 0.00s
Writing names... 0.00s
Writing types... 0.00s
Took 0.09s
Now you can search for functions by type from the command line:
$ hoogle "IO ()"
MyLib someFunc :: IO ()