Pages

Friday, 14 October 2016

Rakefile for Developing Cabal Projects in Haskell

I’ve been struggling with having an adequate Haskell build project for some time now. I’ve gone through hosing my cabal db by treating it like Rubygems and installing every new package. I’ve tried cabal-dev and found it lacking real integration with other Haskell tools. I’ve tried hsenv and had trouble there too.
What finally worked for me is Cabal-1.17.0, which is currently unreleased. This has build in sandboxing support which works great. However, to use GHC-provided tools such as GHCi, you still have to pass flags to point it to your sandboxed environment.
I’m too fond of my editor (vim) to switch to something like Haskell IDE, as impressive as it is. I just want a build process that works, and while the community settles on that, I’ve set up a gist of a Rakefile for common build tasks, and a Guardfile which you can use with Ruby’s guard to run your tests automatically while editing. It even sends notifications to your OS’ notification system. It has worked out really well and runs single-module test suites fast enough that I don’t find myself getting slowed down waiting on test builds too often.

Setup

The whole thing is available in a gist. I will be keeping that up to date if I have to make any changes. You will need ruby and bundler to run it. A process to bring it into an existing project should be like:
cd my-project-dir/
wget -qO- https://gist.github.com/MichaelXavier/5847963/download | tar xz --strip 1
bundle # gem install bundler if you don't have it
rake --tasks # list build tasks
guard start # monitor code and run tests

Install Notes

I make the following assumption with these tools:
  1. You have your project split into src/ and test/ dirs.
  2. You’re using hspec and follow the hspec-discover pattern of having a file test/Spec.hs and that your test dir mirrors your src files, but with Spec.hs at the end. For example: src/Foo/Bar.hs test file will be test/Foo/BarSpec.hs.
  3. You have a recent ruby installed and bundler.
  4. You’re using either the newest cabal with sandboxing or cabal-dev, preferring the former. rake sandbox for example will try to set up a native sandbox with cabal.
  5. Your system supports nproc to determine how many cores to build on. Works on linux. Take it out if that’s not cool with you.
Running rake will build your package. The build task installs dependencies to your sandbox. Likewise, the test task installs test dependencies.

Source

source "http://rubygems.org"
gem "guard-shell"
gem "rake"
view rawGemfile hosted with ❤ by GitHub
guard :shell, :all_after_pass => true do
watch(%r{.*\.cabal$}) do
run_all_tests
end
watch(%r{test/SpecHelper.hs$}) do
run_all_tests
end
def run_all_tests
ncmd("cabal configure && cabal build && cabal test")
end
def ncmd(cmd, msg = cmd)
output = `#{cmd}`
puts output
summary = output.lines.grep(/examples/).first
if $?.success?
n "Build Success!", summary
else
n "Failed", summary
end
end
def run_tests(mod)
specfile = "test/#{mod}Spec.hs"
if File.exists?(specfile)
files = [specfile]
else
files = Dir['test/**/*.hs']
end
if package_db = Dir[".cabal-sandbox/*packages.conf.d", "cabal-dev/*packages.conf.d"].first
package_db_flag = "-package-db #{package_db}"
end
ncmd("ghc -isrc -itest #{package_db_flag} -e 'Test.Hspec.hspec spec' #{files.join(' ')}")
end
# can we join these? why does run all loop through each file?
watch(%r{src/(.+)\.hs$}) do |m|
run_tests(m[1])
end
watch(%r{test/(.+)Spec\.hs$}) do |m|
run_tests(m[1])
end
end
view rawGuardfile hosted with ❤ by GitHub
#!/usr/bin/env rake
def number_of_cores
`nproc`.chomp
end
def build_flags
"-j#{number_of_cores}"
end
desc "run ghci scoped to the sandboxed cabal project"
task :ghci => FileList.new(".cabal-sandbox/*packages.conf.d", "cabal-dev/*packages.conf.d") do |x|
cmd = "ghci -isrc"
if package_db = x.prerequisites.first
cmd += " -package-db #{package_db}"
end
sh cmd
end
desc "sandbox with cabal (requires cabal >= 0.17)"
task :sandbox do
sh "cabal sandbox init"
end
desc "install only essential dependencies. build does this for you"
task :install_dependencies do
sh "cabal install #{build_flags} --only-dependencies"
end
desc "install only essential dependencies. test does this for you"
task :install_dependencies_for_test do
sh "cabal install #{build_flags} --only-dependencies --enable-tests"
end
desc "just build the project for production"
task :build => [:configure, :install_dependencies] do
sh "cabal build #{build_flags}"
end
desc "run tests once. consider using guard afterwards for faster feedback"
task :test => :build_for_test do
sh "cabal test"
end
desc "clean all artifacts generated by cabal"
task :clean do
sh "cabal clean"
end
task :configure do
sh "cabal configure"
end
task :build_for_test => [:configure, :install_dependencies_for_test] do
sh "cabal build #{build_flags}"
end
task :default => :build
view rawRakefile hosted with ❤ by GitHub
from http://michaelxavier.net/posts/2013-07-07-Rakefile-for-Developing-Cabal-Projects-in-Haskell.html