If you're reading this I assume you already love Haskell; so I won't
convince you of why it's great to work in. One thing that isn't so great
is Haskell's story for distributing code to non-haskellers.
stack install
is great, but most folks don't have stack
installed and compiling Haskell projects from source is a lengthy
process. These barriers prevented me from sharing my Haskell projects
for a long time.
Here's how I eventually set up my project to be installed via Homebrew.
We'll be using Homebrew's binary deployment strategy since it's the easiest to both set up and for users to install.
If you're content to build binaries using stack locally and upload them to Github yourself then you can skip down to the Homebrew Formula section.
Building Binaries with Travis-CI
Here's a look at my .travis.yml
:
addons:
apt:
packages:
- libgmp-dev
language: c
sudo: false
cache:
directories:
- $HOME/.local/bin
- $HOME/.stack
os:
- linux
- osx
before_install:
- sh tools/install-stack.sh
- sh tools/install-ghr.sh
script:
- stack setup
- stack build --ghc-options -O2 --pedantic
after_success:
- sh tools/attach-binary.sh
This is just a basic setup for building haskell on Travis-CI; we need
the additional package libgmp-dev
, cache a few things, and
specify to build for both linux and osx. This way we'll have both linux
and osx binaries when we're done! In the pre-install hooks we install
stack manually, then install ghr a github resource
management tool.
You can find install-stack.sh and install-ghr.sh scripts on my Tempered project. They use Travis Env variables for everything, so you can just copy-paste them into your project.
Inside script
we build the project as normal you can do
this however you like so long as a binary is produced.
Lastly is the attach-binary.sh
script. This runs after the build and uploads the generated binaries to
the releases page on Github. It first checks if the current release is
tagged and will only build and upload tagged releases, so make sure you
git tag vx.y.z
your commits before you push them or it
won't run the upload step. Next it pulls in your github token which ghr
will use to do the upload. You must manually add this to your Travis-CI
Environment variables for the project. Create a new github access token
here then add it to
your Travis-CI project at
https://travis-ci.org/<user>/<repo>/settings
under the name GITHUB_TOKEN
.
The script assumes the binary has the same name as your repo, if that's not the case you can hard-code the script to something else. At this point whenever you upload a tagged release Travis-CI should run a mac and a linux build and upload the result of each to your Github Repo's releases page. You'll likely need to trouble-shoot one or two things to get it just right.
Setting up a Homebrew Formula
You can follow this guide by octavore to set up your own homebrew tap; then we'll make a formula. Here's what mine for my tempered project looks like:
class Tempered < Formula
"A dead-simple templating utility for simple shell interpolation"
desc "https://github.com/ChrisPenner/tempered"
homepage "https://github.com/ChrisPenner/tempered/releases/download/v0.1.0/tempered-v0.1.0-osx.tar.gz"
url "9241be80db128ddcfaf9d2fc2520d22aab47935bcabc117ed874c627c0e1e0be"
sha256
:unneeded
bottle
def install
.install "tempered"
binend
test do
system "#{bin}/tempered"
end
end
You'll of course have to change the names, and you'll need to change the url to match the uploaded tar.gz file (for osx) on your github releases page from step one.
Lastly we'll need to get the sha 256
of the bundle; you
can just download it and run shasum -a 256 <filename>
if you like; or you can look in your Travis-CI logs for the osx build
under the attach-binary.sh
step; the script logs out the
sha sum before uploading the binary.
After you've pushed up your homebrew formula pointing to the latest binary then users can install it by running:
# Replace the names respectively
brew update && brew install githubuser/tapname/reponame
Each time you release a new version you'll need to update the url and sha in the homebrew formula; you could automate this as a script to run in Travis if you like; I haven't been bothered enough to do it yet, but if you do it let me know and I'll update this post!
This guide was inspired by (and guided by) Taylor Fausak's post on a similar topic; most of the scripts are adapted from his.
Cheers and good luck!
Hopefully you learned something 🤞! If you did, please consider checking out my book: It teaches the principles of using optics in Haskell and other functional programming languages and takes you all the way from an beginner to wizard in all types of optics! You can get it here. Every sale helps me justify more time writing blog posts like this one and helps me to continue writing educational functional programming content. Cheers!