smlpkg tutorial

by Alex Nelson, 22 August 2024

There are few package managers for Standard ML. In fact, the only one I am aware of is smlpkg which lacks adequate documentation.

This post just constitutes a “note to self”, which I hope may be helpful to others.

Building smlpkg

This requires MLkit, MLton, or any other Standard ML compiler which can work with MLton’s “Basis build system”.

For MLton, you have to build it using the command:

~/src/smlpkg$ MLCOMP=mlton make clean all

Unfortunately, it does not build under Poly/ML or SML/NJ. I tried using Chris Cannam’s sml-buildscripts, but no dice. So if you want to build smlpkg using polybuild, you have extra work to do for yourself.

Installing smlpkg

Assuming you somehow succeeded in building smlpkg…now what?

Well, if you tried make install, it will simply create a subdirectory bin/ inside the smlpkg directory. So, for me, it created ~/src/smlpkg/bin/ and placed a copy of the binary smlpkg there.

You need to either place this in a place which is accessible to $PATH (e.g., in /usr/local/bin/) or you need to add this directory to the $PATH variable.

You can verify you’ve done this correctly by opening up a terminal, and running smlpkg. You should get something like the following:

alex@lisp:~$ smlpkg
Usage: smlpkg [--version] [--verbose] [--help] <command> ...:

Commands:
   add        Add another required package to sml.pkg.
   check      Check that sml.pkg is satisfiable.
   init       Create a new sml.pkg and a lib/ skeleton.
   fmt        Reformat sml.pkg.
   sync       Populate lib/ as specified by sml.pkg.
   remove     Remove a required package from sml.pkg.
   upgrade    Upgrade all packages to newest versions.
   versions   List available versions for a package.

Using smlpkg

Assuming you have now built smlpkg and its directory is accessible to PATH, how do we use it?

As I understand it, the intent is that:

Concluding remarks

Is this worth it? …I feat not so much for me, but hopefully for you.

But this also leaves much to be desired, since smlpkg lacks the functionality of, say, Rust’s cargo or Clojure’s lein (or even Java’s mvn).

For example, there’s no way to run tests with smlpkg, unlike the other tools I listed off. But smlpkg isn’t designed to do that.

There’s a lot of repetitive work involved which I would have expected a package manager to abstract away — for example, whenever I start a new project, I always have to remind smlpkg I use github.com, my username is pqnelson, and it always requires a lib/github.com/pqnelson/<project-name> directory storing all the source code for the project (as opposed to simply requiring a src directory).

And this is entirely hardcoded into smlpkg, by the way.

The design decision seems well-motivated in the abstract: have a lib subdirectory which stores the names of all the websites which have the relevant git repositories, then the usernames are subdirectories of these, and the library needed is the sub-subdirectory. This cleanly separates out dependencies, and prevents name collisions (if the user wanted to use both github.com/pqnelson/xunit and gitlab/some-other-user/xunit, for some reason, then these two packages are always separated from each other).

But it seems like forcing me to manage this in each package I develop is foolish. Why not have a hidden directory ~/.smlpkg/cache/ which does this for me? This avoids duplicate downloads (in case I write a unit testing framework and use it in all my packages, for example: smlpkg would download a copy for each package I develop).

I also don’t understand why smlpkg cannot keep track of those packages and add them to the .mlb build files. This would allow me to develop a package without redundantly bloated github.com/pqnelson/new-package/ subdirectories.