cpkg is now live on Hackage. It is a good deal less polished than I'd originally wanted, but it already gives impressive results (among them cross-compiling XMonad).

Among the more interesting things going on under the hood, it uses Dhall to configure packages as functions.


Cross-Compiling XMonad

First, the exciting bit: we can cross-compile a Haskell project with nontrivial C dependencies! Moreover, despite cpkg's deficiencies, using cpkg is in fact the easiest way to cross-compile e.g. libXrandr. Simply run

cpkg install libX11 --target=arm-linux-gnueabihf cpkg install libXext --target=arm-linux-gnueabihf cpkg install libXrandr --target=arm-linux-gnueabihf cpkg install libXinerama --target=arm-linux-gnueabihf cpkg install libXScrnSaver --target=arm-linux-gnueabihf

and you can then install a cross-compiled XMonad with

cabal new-install xmonad --with-ghc arm-linux-gnueabihf-ghc --with-ghc-pkg arm-linux-gnueabihf-ghc-pkg $(cpkg dump-cabal libX11 libXext libXrandr libXinerama libXScrnSaver --target=arm-linux-gnueabihf)

This is quite exciting. With 1687 lines of Haskell, we get something that surpasses all available solutions!


Configuring a package manager with Dhall is eye-opening. It is now obvious to me that the correct way to configure packages is not with a record but rather with a function.

What Dhall does that is really cool (and which I would hardly have attempted to implement myself prior to seeing it) is that it allows such functions to be lifted to Haskell generically.

As an example, if you have a Dhall file containing

let coolFunction = λ(x : Text) → "${x}-gcc" in


You can lift this to a function Text -> Text in Haskell as follows:

liftedCoolFunction :: IO (T.Text -> T.Text) liftedCoolFunction = inputFile auto "coolFile.dhall"

As an example package, consider

{- cpkg prelude imports -} let types = https://raw.githubusercontent.com/vmchale/cpkg/master/dhall/cpkg-types.dhall in

let prelude = https://raw.githubusercontent.com/vmchale/cpkg/master/dhall/cpkg-prelude.dhall in

let lua = λ(v : List Natural) → let printLuaOS = λ(os : types.OS) → merge { FreeBSD = λ(_ : {}) → "freebsd" , OpenBSD = λ(_ : {}) → "bsd" , NetBSD = λ(_ : {}) → "bsd" , Solaris = λ(_ : {}) → "solaris" , Dragonfly = λ(_ : {}) → "bsd" , Linux = λ(_ : {}) → "linux" , Darwin = λ(_ : {}) → "macosx" , Windows = λ(_ : {}) → "mingw" , Redox = λ(_ : {}) → "generic" , Haiku = λ(_ : {}) → "generic" , IOS = λ(_ : {}) → "generic" , AIX = λ(_ : {}) → "generic" , Hurd = λ(_ : {}) → "generic" , Android = λ(_ : {}) → "generic" , NoOs = λ(_ : {}) → "c89" } os in

let luaBuild =
  λ(cfg : types.BuildVars) →
    let cc = prelude.mkCCArg cfg

    let ldflags =
      (prelude.mkLDFlags cfg.linkDirs).value

    let cflags =
      (prelude.mkCFlags cfg.includeDirs).value

    let os =
      prelude.osCfg cfg

    [ prelude.call (prelude.defaultCall ⫽ { program = "make"
                                          , arguments = cc # [ printLuaOS os, "MYLDFLAGS=${ldflags}", "MYCFLAGS=${cflags}", "MYLIBS=-lncurses", "-j${Natural/show cfg.cpus}" ]

let luaInstall =
  λ(cfg : types.BuildVars) →
    [ prelude.call (prelude.defaultCall ⫽ { program = "make"
                                          , arguments = [ "install", "INSTALL_TOP=${cfg.installDir}" ]
      # prelude.symlinkBinaries [ "bin/lua", "bin/luac" ]

prelude.simplePackage { name = "lua", version = v } ⫽
  { pkgUrl = "http://www.lua.org/ftp/lua-${prelude.showVersion v}.tar.gz"
  , configureCommand = prelude.doNothing
  , buildCommand = luaBuild
  , installCommand = luaInstall
  , pkgDeps = [ prelude.unbounded "readline"
              , prelude.unbounded "ncurses"


It's all a bit difficult to grasp at first, but this style of configuration - as a function rather than a record - is leagues ahead of anything else out there.

Finally, Turing completeness turns out to be nice. Maybe it's too strong, but a limited language is clearly good: you can easily see what belongs in the Dhall library and what should be implemented on the Haskell side.


Packaging C

Packaging for C is pretty appalling for those of us used to cabal new-build and Hackage. Most packages are distributed via tarballs, but not all are distributed via HTTPS. alsa is only available via FTP.

There are three build systems commonly used (autotools, cmake, and meson) and each needs to be supported in the project's Dhall library. meson is written in Python and it is particularly hard to use as a build dependency without polluting any global installations.

Some projects choose to forgo these and just use a plain Makefile, which generally needs to be written in a project-specific way - the Dhall configuration for Lua is 64 lines. 21 lines are dedicated to printLuaOs,

let printLuaOS = λ(os : types.OS) → merge { FreeBSD = λ(_ : {}) → "freebsd" , OpenBSD = λ(_ : {}) → "bsd" , NetBSD = λ(_ : {}) → "bsd" , Solaris = λ(_ : {}) → "solaris" , Dragonfly = λ(_ : {}) → "bsd" , Linux = λ(_ : {}) → "linux" , Darwin = λ(_ : {}) → "macosx" , Windows = λ(_ : {}) → "mingw" , Redox = λ(_ : {}) → "generic" , Haiku = λ(_ : {}) → "generic" , IOS = λ(_ : {}) → "generic" , AIX = λ(_ : {}) → "generic" , Hurd = λ(_ : {}) → "generic" , Android = λ(_ : {}) → "generic" , NoOs = λ(_ : {}) → "c89" } os in


which serves no interesting function - it just translates from cpkg's OS type to something suitable for Lua's Makefile.

It is clear that many build systems for prominent C projects were not designed to be automated.

In addition, many prominent libraries contain bugs. m4 fails to build with the latest glibc. npm builds will hang indefinitely, but only when you don't look at the terminal output.

Such things are papered over by Linux distributions, which generally offer tested binary packages, but it's an unfortunate situation - I suspect that with more automated build tools like cpkg, library releases would contain fewer bugs!

Dhall's Performance

Dhall's performance is poor. Some of this is fixable - it uses Text when it should intern identifiers, for instance - but imports via HTTP are not. Most of the marketing for Dhall touts it as a replacement for YAML/JSON, so it's not too surprising that it's not perfectly suited for package configuration.

What I would really like to see here is something that takes the good parts of Dhall (types, Turing incompleteness, functions, fluent Haskell integration) and makes it into a full-blown package configuration language. I'm inclined to think something with its own package manager and "compiled" libraries is the solution, but really anything that doesn't make HTTP requests every time a file is interpreted would be an improvement.

I think it would be worth adding a real module system and better type inference in the process, but that I am less sure of.

Tar Package

The Haskell tar package is quirky, to say the least. Among other things, it chokes on the gcc and llvm tarballs - even if it's doing the right thing here (based on standards), there are enough prominent projects to make things pretty frustrating.


Package Database

The global package database is not actually implemented as a database, but rather a single file that contains a serialized S.Set BuildCfg. It would be better to store this in a real database, for the sake of performance and resiliency.

The package database also doesn't contain enough information to do nix-style garbage collection, which would be required for cpkg to be considered a full-fledged package manager in the vein of apt or nix.

Dependency Resolution

I didn't bother to implement any sort of dependency resolution. The current layout of the code shouldn't prevent anyone from doing so, but, it is still a deficiency.

With dependency resolution, we'd get something like cabal new-build for C/C++, which would be really exciting. Many C libraries use weird preprocessor hacks to support multiple versions of some library - none of this is necessary if you don't require that only one version of a particular library be installed at a time.