Using Cabal on its Own

cabal_packages.jpg

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!

Previous
Previous

Nicer Package Organization with Stack!

Next
Next

Organizing Our Package!