User Guide: rules_ocaml Last updated June 1, 2022

The OBazl Ruleset(s)

Core collection of primitives (build rules) that can be composed to accomplish any build task.

The most basic build tasks are compilation of interface and implementation files, linking of archive files, and linking of executables. OBazl includes one rule for each of these tasks, named accordingly: ocaml_signature, ocaml_module, ocaml_archive, and ocaml_executable.

Most real-world projects involve some additional tasks:

  • preprocessing of source files, including but not limited to PPX transformations;

  • renaming of source files to add a namespace prefix;

  • generation of a resolver module (sometimes called a "map" file) containing the module aliasing equations required to make namespacing ("wrapped" libraries in Dune) work.

None of these tasks involve the OCaml toolchain, so (following the Principle of Parsimony), OBazl does not include rules for them. They can all accomplished using the standard genrule ("general rule") provided by Bazel.

Here is an example of a simple build pipeline using only OBazl primitives, to compile a namespaced module with a PPX transformation:

source file -> genrule to rename src -> genrule to execute ppx transform -> ocaml_module

In this example, the first step would use a shell command (cp or ln), and the second step, running a PPX transform, would depend on the output of another pipeline ending in an ocaml_executable that produces the PPX executable. Both genrule steps would involve a shell command that must be written by the programmer.

In this case Bazel would function more or less as a glorified Make; it would analyze dependencies and invoke the build actions required by a change in sources, but would delegate actual build responsibility to the shell scripts written in the genrules (except for the final ocaml_* rule). So this is not something one would do in practice. Nonetheless, in principle such composable pipelines could be used to build any OCaml project. But build files written at such a low level of detail would be tedious to write, error-prone (since they involve shell scripting), verbose, and hard to maintain. So in order to meet its design goals (Ease of Use, etc.), OBazl extends some of its rules to automate the most common build patterns in a more convenient and expressive manner, and to take advantage of the Bazel’s specialized build API. For example, instead of passing the PPX executable to a genrule (which runs a shell command that the developer must write) that runs it as a separate task, we can pass it directly to the ocaml_module rule via its ppx attribute, which has the effect of directing the rule to run the transform and compiles output. The transform will still be executed as a separate build action, but it will be managed by the ocaml_module rule, so the developer doesn’t have to bother directly with the details of a shell command. The OBazl rules have also been extended to automate namespacing, so that the developer is responsible only for annotating the rules with attributes indicating namespace membership, and OBazl takes care of the rest.

NB: compositionality: rules v. build actions

OBazl includes two rulesets:

  • the standard ruleset (rule names prefixed by ocaml_ or ppx_)

  • tools_ocaml - rules for third-party tools like cppo, menhir, etc.

rules_ocaml

The standard rules_ocaml ruleset can be thought of as a layer that sits on top of and extends the bootstrap ruleset. All the added functionality could be implemented using generic Bazel facilities (genrules, macros, custom rules), but OBazl provides built-in support for the most commonly needed to ensure ease-of-use etc.

The point being that Bazel (Starlark) is the build language, OBazl just uses it to define rules that make life easier, and the programmer always has access to the full power Bazel. I.e. you’re not limited in to the functionality OBazl supports out-of-the-box. Contrast Dune, which does not build on a lower-level build DSL in this way.

It adds support for:

  • PPX processing, including automated management of so-called "runtime dependencies"

  • Generalized namespacing (automatic generation of "ns resolver" modules) to compliment the automatic module renaming supported by the bootstrap rules.

  • Contingent dependencies - selection of dependencies based on configuration state

    • corresponds to Dune’s "alternative dependencies" using (select …​ from …​)

    • no special syntax or functionality is involved; dependencies may be selected using Bazel’s standard, generic select function

    • NB: this is just a matter of using Bazel’s select function for deps, so it is available in the bootstrap ruleset.

  • Full control over module bindings

    • A module rule can select any implementation file for binding to any particular signature (.cmi) file, based on configuration settings; for example, binding clock.cmi to a platform-specific implementation e.g. clock_linux.ml is expressible using a simple select statement on a single ocaml_module target.

    • Eliminates need for "virtual libraries". Module bindings like this need not be delayed to link-time.