Split Dependencies Last updated June 2, 2022

OCaml file-system modules are built from two kinds of file: a structfile (usually with a .ml extension) and optionally a sigfile (extension .mli). OBazl defines a build rule for each: for structfiles, ocaml_module; for sigfiles, ocaml_signature.

Dyadic file-system modules may be built using an ocaml_module rule alone, or using both an ocaml_module and an ocaml_signature rule. In the former case, the name of the sigfile is passed using the sig attribute; in the latter case, the sig attribute will be passed the name of the ocaml_signature target.

This affords fine-grained control over all build paramters. If a dyadic module is built using just an ocaml_module rule, then the same build options will be used to compile both files. If an ocaml_signature rule is also used, then the structfile and sigfile may be compiled with different options.

Use of ocaml_signature also affords split dependency graphs. Structures and signatures may have their own dependency graphs, so it is possible to optimize them; for example in some cases sigfiles will only depend on other sigfiles, and this can be easily expressed with OBazl. Standard Bazel query facilities can then show the separate dependency graphs. Optimizing dependency graphs in this way can have performance implications for builds. Implementations depend on interfaces, but (pure) interfaces need not depend on implementations. If interface dependencies are expressed as module dependencies (that is, dependencies on both a signature and a structure), then changes to the structfile will trigger recompilation of all modules that depend on the struct (and hence all modules that depend on those interfaces), even if they really only depend on its interface. If interfaces only depend on interfaces, then changes to a structfile will not trigger a rebuild of interfaces.

The downside of one-rule-per-source-file is that you need one rule per source file. Many (most?) OCaml build systems support some form of "dynamic" dependency resolution, so that the user need not bother with a complete listing of inputs. Genuine dynamic dependency discovery is disallowed by Bazel, since it is incompatible with Bazel’s primary goal of ensuring replicable ("hermetic") builds. On the other hand, Bazel does support configurable dependencies - dependencies that are resolved during the initial analysis phase of a build - but only if they resolve to resources that were already registered as build inputs. The classic case is selection of a platform-dependent source file at build time. The selected file will not be known before building, but the set of files from which the selection must be made will be known before the build commences.

Writing one rule per source file is an obvious candidate for automation. Version 1 of OBazl did not include any tools to handle this, but version 2 includes one tool to generate BUILD.bazel files from a tree of source files, and another to convert dune files to BUILD.bazel files. To assist with ongoing maintenance, it also includes a batch editing tool that allows developers to script the editing of BUILD.bazel files.


Split dependencies

OCaml interface and implementation files for a given module may have very different dependency graphs.

Since OBazl supports separate builds of .ml and .mli files, users can optimize by listing (as appropriate) only cmi deps for an mli file. Note that dep analysis tools like ocamldeps and codept will tell you which modules an interface file depends on, but will not indicate whether the dependency is in fact only on the .cmi file; so this kind of optimization must generally be done by hand.

Since modules depend on sigs, but not the other way around, this means that signature dep graphs can be built without causing the build of any modules, and queries can show just the signature dependency graph of a target.