Using Cabal on its Own
Last week we discussed the format of the .cabal
file. This file is a "package description" file, like package.json
in a Node.js project. It describes important aspects of our project's different pieces. For instance, it tells us what source files we have for our library, and what packages it depends on.
Of course this file is useless without a package manager program to actually build our code! These days, most Haskell projects use Stack for package management. If you want to jump straight into using Stack, take a look at our free Stack mini-course!
But Stack is actually only a few years old. For much of Haskell's history, Caba was the main package management tool. It's still present under the hood when using Stack, which is why Stack still uses the .cabal
file. But we can also use it in a standalone fashion, as Haskell developers did for years. This week, we'll explore what this looks like. We'll encounter the problems that ultimately led to the development of Stack.
If you're still new to the Haskell language, you can also read our Liftoff Series to get a better feel for the basics!
Making a New Project
To create a new project using Cabal, you should first be in a specific directory set aside for the project. Then you can use cabal init -n
to create a project. This will auto-generate a .cabal
file for you with defaults in all the metadata fields.
>> mkdir MyProject && cd MyProject
>> cabal init -n
>> vim MyProject.cabal
The -n
option indicates "non-interactive". You can omit that option and you'll get an interactive command prompt instead. The prompt will walk you through all the fields in the metadata section so you can supply the proper information.
>> mkdir MyProject && cd MyProject
>> cabal init
Package name? [default: MyProject]
Package version? [default: 0.1.0.0]
...
Since you only have to run cabal init
once per project, it's a good idea to run through the interactive process. It will ensure you have proper metadata so you don't have to go back and fix it later.
Running this command will generate the .cabal
file for you, as well as a couple other files. You'll get ChangeLog.md
, a markdown file where you can record important changes. It also generates Setup.hs
. You won't need to modify this simple boilerplate file. It will also generate a LICENSE
file if you indicated which license you wanted in the prompt.
Basic Commands
Initializing the project will not generate any Haskell source files for you. You'll have to do that yourself. Let's suppose we start with a simple library function in a file src/Lib.hs
. We would list this file under our exposed-modules
field of our library section. Then we can compile the code there with the cabal build
command.
If we update our project to have a single executable run-project
, then we can also run it with cabal run
. But if our project has multiple executables, this won't work. We'll need to specify the name of the executable.
>> cabal run run-project-1
"Hello World 1!"
>> cabal run run-project-2
"Hello World 2!"
You can also run cabal configure
. This will do some system checks to make sure you can actually build the program. For instance, it verifies that you have some version of ghc
available. You can also use the command to change this compiler version. It also does some checks on the dependencies of your package.
Adding Dependencies
Speaking of dependencies, let's explore adding one to our project. Let's make our library depend on the split
package. Using this library, we can make a lastName
function like so:
import Data.List.Split(splitOn)
lastName :: String -> String
lastName inputName = splitOn " " inputName
When we try to build this project using, we'll see that our project can't find the Data.List.Split
module...yet. We need to add split
as a dependency in our library. To show version constraints, let's suppose we want to use the latest version, 0.2.3.3
at the time of writing.
library:
build-depends:
base >=4.9 && <4.10
, split == 0.2.3.3
...
This still isn't quite enough! We actually need to install the split
package on our machine first! The cabal install
command will download the latest version of the package from hackage and put it in a global package index on our machine.
cabal install split
Once we've done this, we'll be able to build and run our project!
Conflicting Dependencies
Now, we mentioned that our package gets installed in a "global" index on our machine. If you've worked in software long enough, this might have set off some alarm bells. And indeed, this can cause some problems! We've installed version 0.2.3.3 globally. But what if another project wants a different version? Configuring will give an error like the following:
cabal: Encountered missing dependencies:
split ==0.2.3.2
And in fact it's very tricky to have both of these versions installed in the global index!
Using a Sandbox
The way around this is to sandbox our projects. When we do this, each dependency we get from Hackage will get installed in the project-specific sandbox, rather than a global index. Within our project directory, we can create a sandbox like so:
>> cabal sandbox init
Now this project will only look at the sandbox for its dependencies. So we'll see the same messages for the missing module when we try to build. But then we'll be fine when we install packages. We can run this version of the command to install all missing packages.
>> cabal install --only-dependencies
Now our project will build without issue, even with a different version of the package!
Conflicts within a Project
Let's consider another scenario that can introduce version conflicts. Suppose we have two dependencies, package A and package B. Each of these might depend on a third package, C. But package A might depend on version X of package C. And then B might depend on version Y of C. Because we can't have two versions of the package installed, our program won't build!
Sandboxes will prevent these issues from occurring across different projects on our machine. But this scenario is still possible inside a single project! And in this case, we'll have to do a lot more manual work than we bargained for. We'll have to go through the different versions of each package, look at their dependencies, and hope we can find some combination that works. It's a messy, error-prone process.
Next week, we'll see how to solve this issue with Stack!
Conclusion
So we can see now that Cabal can stand on its own. But it has certain weaknesses. Stack's main goal is to solve these weaknesses, particularly around dependency management. Next week, we'll see how this happens!