Building OxCaml packages for Debian, Fedora, Homebrew and Arch

Native OxCaml system packages for Debian/Ubuntu, Fedora, Arch and Homebrew — plus reviving a 2013 GPG key that modern tooling rejects for SHA-1, and using agentic coding to collapse the opam build into one tarball.

I needed to install OxCaml quickly on a fresh machine without any opam machinery. This was an excellent excuse to refresh my memory on how distributing system packages for distributions like Debian, Arch, Fedora and Homebrew work.

I've built oxcaml-pkgs to churn out native packages for the distros I need, with details below for future reference.

These all install an oxcaml-compiler package into /opt, since anywhere else would clash with the native OCaml packaging. The idea is that a consumer can add this to their PATH to specifically get OxCaml. However, I don't intend most people to do this manually as I'm wrapping this in my oi at the moment.

1 The quick installer script

If you just want OxCaml and don't care how, there's a one-shot installer:

curl -fsSL https://oi.thicket.dev/repo/install.sh | sh

The rest of this note is the manual breakdown of what that script automates, mostly so I remember how each packaging system works the next time I revisit it!

2 Debian / Ubuntu

curl -fsSL https://oi.thicket.dev/repo/apt/oxcaml.asc | sudo gpg --dearmor -o /usr/share/keyrings/oxcaml.gpg
# pick your release: noble, resolute, or trixie
echo 'deb [signed-by=/usr/share/keyrings/oxcaml.gpg] https://oi.thicket.dev/repo/apt resolute main' | sudo tee /etc/apt/sources.list.d/oxcaml.list
sudo apt update && sudo apt install oxcaml-compiler

Debian and Ubuntu both maintain packaging metadata in a debian/ directory with various bits of metadata, e.g.:

Source: oxcaml-compiler
Section: devel
Priority: optional
Maintainer: @MAINTAINER@
Build-Depends: debhelper-compat (= 13),
 gcc, g++, make, m4, autoconf, perl, rsync, tar, gzip, bzip2, pkg-config, zstd
Standards-Version: 4.7.0
Homepage: https://oxcaml.org
Rules-Requires-Root: no

Package: oxcaml-compiler
Architecture: any
Depends: ${misc:Depends}, libc6

This is then compiled from a source package to an architecture-specific binary one that has a .deb extension. That's done via scripts that invoke pbuilder in a Docker container for the exact Ubuntu or Debian distro.

I would have used Launchpad as I used to do for opam PPAs back in the day, but it's currently down due to a DDoS so I'm building these myself for now.

3 Fedora 44

sudo tee /etc/yum.repos.d/oxcaml.repo >/dev/null <<'EOF'
[oxcaml]
name=OxCaml
baseurl=https://oi.thicket.dev/repo/rpm/fedora-44
enabled=1
gpgcheck=0
repo_gpgcheck=1
gpgkey=https://oi.thicket.dev/repo/rpm/fedora-44/oxcaml.asc
EOF
sudo dnf install oxcaml-compiler

Fedora's got the DNF package manager, which uses spec files to wrap the build. These source RPMs are then compiled to binary ones via a mock build in a Docker container for that distro.

Once that's done, the repository metadata is assembled using createrepo, and a bunch of files that can be served over HTTP.

4 Arch Linux

curl -fsSL https://oi.thicket.dev/repo/arch/oxcaml.asc | sudo pacman-key --add -
sudo pacman-key --lsign-key <keyid>
sudo tee -a /etc/pacman.conf >/dev/null <<'EOF'

[oxcaml]
SigLevel = Required DatabaseOptional
Server = https://oi.thicket.dev/repo/arch/$arch
EOF
sudo pacman -Sy oxcaml-compiler

Arch uses PKGBUILD files for its metadata format, which makepkg compiles into a simple .pkg.tar.zst layer in an archlinux container; repo-add then stitches them into a pacman database.

5 Homebrew

My custom Homebrew instructions from yesteryear still work fine, so I just pushed a Homebrew OxCaml formula there and let the brew bot do its magic.

The release flow here is two stage:

  • tests.yml runs brew test-bot on every PR, building the formula and uploading the resulting bottles as CI artifacts.
  • Then when I manually add the pr-pull label to that PR, publish.yml fires on the labeled event, runs brew pr-pull to download those built bottles, commits them with the formula to main, pushes, and deletes the branch.

The only quirk here is to not link it into /opt/homebrew as it would collide with OCaml, so it's marked "keg only" (installed but not symlinked into the prefix). I think we can integrate the brew bottling directly into obuilder just as soon as we add secrets support to the macOS backend.

6 Resurrecting a 2013 GPG key

I did also have to do some GPG shenanigans as my ancient Debian signing key from 2013 is now rejected because its crypto signature used SHA-1. Modern gpg, apt and pacman reject SHA-1 certifications outright now as being too weak, so reprepro refused to trust me anymore.

Upgrading the key gracefully (rather than minting a brand new identity and losing decades worth of signatures) turned out to be fiddly, and I have only the haziest memory of modern keyserver etiquette. The last time I looked at this seriously was chatting to Yaron Minsky about fifteen years ago about his SKS OCaml keyserver!

Anyway, I have a fresh ed25519 signing subkey now, so I'll properly rebuild the web of trust later on. Maybe tangled.org's new vouching system would be a good place to anchor a PGP web of trust again.

7 Reproducing the OxCaml opam directives

The most fiddly part of all this was reproducing the OxCaml build exactly as its opam directives would, but without opam in the loop and just a single unified tarball where you can do a make && make install. Distros typically abhor other package managers...

I used a fair amount of agentic coding here: I pinned the oxcaml-compiler.5.2.0minus31 opam package and had Claude resolve the patch list and build/install steps into a single shell script, then verified that the resulting unified patches were byte-identical to what opam would have assembled.

This is also all fiddly enough that it makes me want to investigate package repositories on ATProto more now...

References

[1]Madhavapeddy (2025). How to publish custom Homebrew taps for OCaml. 10.59350/sf0ze-pbf15