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.
See also Modules, Separate Compilation