B0 manual

This manual provides a conceptual overview of the B0 system and explains the basic mechanics of B0 files and their definitions. See here for other topics.

Introduction

B0 aims at liberating the programmer from the essential but tedious and brittle bureaucracy that surrounds the programming activity. It provides a system with a decent description language to devise modular and custom solutions to software construction and deployment problems.

B0 describes:

These descriptions are made in B0 files which we describe next.

B0 files

A B0 file is a syntactically valid OCaml file possibly prefixed by a few directives. The OCaml code creates definitions to describe the software by using the versioned B0_kit API and possibly other third-party OCaml libraries.

A B0 file is named B0.ml and is usually found at the root of the software project's source tree it describes.

The following is a simple B0.ml file that declares an executable to build from the OCaml src/hello.ml source file and that uses the cmdliner OCaml library.

open B0_kit.V000

let cmdliner = B0_ocaml.libname "cmdliner"
let hello =
  let doc = "Greeting tool" in
  let srcs = [`File ~/"src/hello.ml"] in
  let requires = [cmdliner] in
  B0_ocaml.exe "hello" ~srcs ~requires ~doc

The definitions of B0 files are consulted and processed by drivers. The b0 tool distributed with B0 is an examples of a driver. An API is provided to create custom driver, details can be found in the B0 driver development manual but you likely will not need this.

Root B0 file

The B0 file executed by a driver is called the root B0 file. When a driver like b0 is invoked in a current working directory cwd, the root B0 file is the first B0.ml file found starting at the cwd and moving upwards in parent directories.

The root B0 file can also be specified explicitely via the --b0-file option or the B0_FILE environment variable. The b0 file command allows to find out which root B0 file is determined by b0 and provides a few actions on it:

b0 file path    # Show the absolute path to the root B0 file
b0 file edit    # Edit the root B0 file.
b0 file --help  # See all actions available on the B0 file

Root directory

The B0 root (directory) is the directory in which the root B0 file is found. The b0 root command shows that directly:

b0 root  # Show the absolute path to the root directory
         # Effectively this is dirname $(b0 file path)

Most of the time the root directory is the root of a project's source file tree. However as we will see below, a B0 file can gather multiple project definitions by including other B0 files. In this case the root directory may be unrelated to the directory of these B0 files and may well be empty.

Sometimes it may be useful to lock b0 invocation on a particular B0 root. For example so that you can move into your file system and still have b0 lookup and run entities described therein. This can be achieved by specifying the B0_FILE and B0_DIR environment variables. The b0 lock and b0 unlock commands make it easy to set them to the right values:

b0 -- hello ~/elsewhere/input  # Running the hello tool
eval $(b0 lock)                # Lock b0 on the current B0 root
cd ~/elsewhere
b0 -- hello input              # Works as the first invocation above
eval $(b0 unlock)              # Unlock a locked B0 root

_b0 directory

The root directory is where the scratchable _b0 directory used by drivers to operate and store their results gets created by default. The location of the _b0 directory can also be specified explicitely via the --b0-dir option or the B0_DIR environment variable. _b0 directories should be ignored by version control systems:

echo _b0 >> .gitignore  # Let git ignore the _b0 directory
echo _b0 >> .hgignore   # Let hg ignore the _b0 directory

Scope directory and relative file paths

In the B0 file we gave above the src/hello.ml file is relative to the directory of the B0 file in which we wrote the definition. Unless otherwise indicated, relative file paths specified in definitions and directives of a given B0 file are always relative to the directory of that B0 file.

This directory is called the scope directory of a B0 file. Sometimes the scope directory coincides with the root directory but that is not always the case due to B0 file inclusion.

Including B0 files

The @@@B0.include directive provides a mecanism to compose B0 files. The intent of B0 file inclusion is to be able to aggregate independently described software components for common testing or development or to vendor dependencies. Include directives must be specified at the start of a B0 file, before comments or OCaml code.

For example the following B0.ml file includes the definitions of two other B0 files:

[@@@B0.include "that" "../that-lib/B0.ml"]
[@@@B0.include "projects/proj/B0.ml"]

The b0 file gather command can be used to quickly gather a a bunch of B0.ml files from directories in a directory and symlink the directories therein:

b0 file gather -s -d /tmp/gather /path/to/repos/*

Scopes

An included B0 file defines a named scope to which its definitions are added. Scope names are either specified explicitely or automatically derived from the directory name of the B0 file. In the example above the scope names are, in order, that and proj.

From the B0 file that includes, the syntax to access a definition named d in a scope s is s.d.

Scope names must be unique in a given B0 file and must not contain '.' characters. Definitions made in libraries are in their own library scope which is prefixed by ..

XXX Idea: let local toplevel scopes and global scope clash and use .bla to disambiguate if that is the case (and let . denote the root scope).

Access control and isolation

At definition time a B0 file can only access definitions that are in its own scope or in the scope of the files it @@@B0.includes and recursively. This means that in any B0 file:

let all_units = B0_unit.list () (* get all units defined so far *)

the value all_units depends only on the definitions and includes of the B0 file itself, however included it may itself be.

In the example above the two included B0 files cannot refer to the names of the other directly at definition time. However at build time their definitions may interact indirectly via metadata and build logic name resolution procedures which have access to the global scope, see Dependency vendoring.

XXX Say that a bit differently. Build logics may define their own namespaces and resolution logics. E.g. the OCaml build logic defines a notion of library name and library lookup as per ocamlfind. Build units that may not see each other directly may interact via these names.

XXX. Since actions can access the build this is not entirely true. We likely need to clarify something along the static or definitional and dynamic aspect.

Syntactically, the OCaml scope of included files is not accessible and always fully isolated: both includer and includee cannot access each other's OCaml identifiers. You can think of them as being wrapped in their own module _ : sig end module.

Dependency vendoring

Support for build dependency vendoring depends on the name resolutions procedure of the build logic you are using, there is no specific support for it in B0 except for the inclusion mecanism which allows to gather definitions from other B0 projects.

If the build logic you use is able to look in the definitions of the root B0 file for dependencies and use these instead of those found in the build environment then vendoring is simply a matter of @@@B0.including the definitions of the dependencies to your B0 file.

A typical setup is to have at the root of a project:

B0.ml
vendor/dep1/
vendor/dep2/
src/

and to start your project's B0.ml file with:

[@@@B0.include "vendor/dep1/B0.ml"]
[@@@B0.include "vendor/dep2/B0.ml"]
...

Since dependencies may have vendored dependencies themselves, coherence issues may arise. You may have to define new consistent and locked build packs in the root B0 file in order to exclude conflicting dependencies.

XXX What about introducing @@@B0.exclude directives to exclude scopes (by name).

Using libraries

B0 is a modular and extensible system. It is expected for third-parties to define build and deployment logics and distribute them as libraries.

Any OCaml library available in the environment in which b0 is run can be used in a B0 file by requiring it via the #require directive. #require directives must be specified at the start of the file, before comments or OCaml code.

For example:

#require "mylib"

instructs to lookup the OCaml library mylib in the environment in which the b0 tool is run it to compile the B0 file.

Custom local descriptions

Project specific descriptions and build logics should not clutter your B0 files. In case they become unwiedely it's a good idea to put these in their own file an simply import the source in the B0 file via the the #mod_use directive.

#mod_use directives must be specified at the start of the file before comments or OCaml code and they can't define B0 definitions, only functions that produce them. TODO sort out scope/allow defs or not.

By convention these definitions should be in a B0 directory alongside the B0.ml file. For example here we have support for that-tool in B0/that_tool.ml.

#mod_use "B0/that_tool.ml"

let hello =
  let doc = "hello exe produced by that-tool" in
  let srcs = ["src/bla.tt"] in
  That_tool.exe ~name:"hello" ~doc ~srcs

The #mod_use directive simply creates a module with that file name and the contents of the file spliced in there. If present a corresponding B0/that_tool.mli file constrains the interface of that module accordingly.

B0 file bootstrap

A B0 file is an OCaml source and may need additional OCaml libraries. This means it needs an OCaml compiler in the user PATH and the #required libraries in the OCAMLPATH.

Making sure these libraries are available is the purpose of B0 file boostrapping. The @@@B0.boot directive allows to specify a system to run in order to install the B0 file prerequisites.

For example the following directive:

[@@@B0.boot "opam" "my-logic>=1.0.0"]

indicates the B0 file needs the my-logic package to be installed with version greater or equal than 1.0.0. The syntax for opam package names and constraints follows that of the opam install command.

This declaration allows to install the B0 file prerequisites by invoking:

b0 file boot --opam

B0 file definitions

This section provides a high-level view of the definitions made in B0 files to describe the software.

B0 definitions are named and static. The latter means they need to be defined during the initialisation of the B0 file. This allows end-user to query and act upon them from the command line or other user interfaces.

Metadata

All definitions in B0 have a metadata dictionary of type B0_meta.t attached. Metadata is used to drive build logics, deployments and custom procedures.

The type B0_meta.t is a simple type-safe heterogenous value map. A few standard keys are provided, consider using these before defining your own. Also consult B0_kit and libraries of build logic which may define more.

Build units

A build unit gathers sets of related low-level build operations (e.g. tool spawns). Typical build units are sequences of commands that build a library, an executable, etc.

Build units structure builds in well identified fragments, they form the smallest unit of build that can be be requested for building by the user.

They are given a build directory in _b0 according to the build environment and their name where they should – but are not required – to output their results.

Read the build unit manual.

Build packs

A build pack gathers a set of build units. Build packs have no formal operational functionality in the system. It is just a way to list build units under a name and perform coarse grained actions on them. Note that a build unit can belong to more than one pack.

Here are example of pack usage in the system:

The default pack

The default pack defines the build units that are built when a bare b0 build is invoked.

Build environments

The default environment

The user environment

Actions

Actions are convenience named commands you can define and directely invoke via

b0 -- ACTION …

If your development practice needs a bit of shell scripting here and there, consider rewriting them in a decent programming language as cmdlets.

Read the action manual.

Editor support

Emacs

The logic below guesses a compilation command the first time you invoke compilation mode depending on the current buffer file.

(require 'compile)

(defun guess-compile-command ()
  "Guess a compilation command from the buffer file"
  (if buffer-file-name
      (cond
       ((locate-dominating-file buffer-file-name "B0.ml") "b0 ")
       ((locate-dominating-file buffer-file-name "dune-project")
        "dune build ")
       ((locate-dominating-file buffer-file-name "Makefile") "make ")
       ((locate-dominating-file buffer-file-name "BRZO") "brzo -b ")
       (t
        (concat "brzo --root " (file-name-directory buffer-file-name) " -b ")))
    ""))

(setq compile-command '(guess-compile-command))

B0 file reference

The following parts can be distinguished in a B0 file:

  1. The initial sequence of directives. Before any comment or OCaml construct.
  2. The OCaml unit implementation.

The order is important. Directives that are mentioned after the OCaml unit implementation starts are either silently ignored (@@@B0.* directives) or produce compilation errors (#* directives).

Syntax

A B0 file is white space (no comments) separated directives followed by an OCaml unit implementation.

Using an RFC 5234 grammar this reads as:

b0_file    = *(ws directive) ws unit-implementation
directive  = dir-boot / dir-inc / dir-req
dir-boot   = "[@@@B0.boot" *(ws dstring) ws "]"
dir-inc    = "[@@@B0.include" *(ws dstring) ws "]"
dir-req    = "#require" ws dstring
dir-moduse = "#mod_use" ws dstring
dstring    = %x22 dchar *dchar %x22
dchar      = escape / cont / ws / %x21 / %x23-%x5B / %x5D-%x7E / %x80-xFF
escape     = %x5C (%x20 / %x22 / %x5C)
cont       = %x5C nl ws
ws         = *(%x20 / %x09 / %x0A / %x0B / %x0C / %x0D)
nl         = %x0A / %x0D / %x0D %x0A
unit-implementation = ... ; See the syntax in the OCaml manual

Directive @@@B0.boot

The syntax of the @@@B0.boot directive is:

[@@@B0.boot "SYSTEM" "ARG"... ]

At the moment the only known value for "SYSTEM" is "opam" and its argument names are packages constraints using the same syntax as the opam install command:

[@@@B0.boot "opam" "PKG"... ]

This indicates the opam package PKG need to be installed in order to compile the B0 file.

Directive @@@B0.include

The syntax of the @@@B0.include directive is one of:

[@@@B0.include "PATH"]
[@@@B0.include "NAME" "PATH"]

The semantics is to include in the B0 file the definitions of the B0 file at "PATH" in a scope named "NAME". If "NAME" is is not specified the directory name of the included B0 file is used.

"NAME" must be unique among the B0 files included in the file and must not contain '.' characters.

Directive #require

The syntax of the #require directive is:

#require "LIB"

The semantics is to compile and link the B0 file against library LIB, looked up in the OCAMLPATH. At the moment library LIB's dependencies must be manually #required aswell.

Directive #mod_use

The syntax of the #mod_use directive is:

#mod_use "PATH"

The semantics is to define a module with the contents of PATH at that location. The name of the module is defined by mangling the file name of PATH.

For example assuming the PATH is B0/file.ml, the directive expands to:

module File = struct
#1 "B0/file.ml"
(* contents of B0/file.ml *)
end

If PATH has a corresponding .mli file in the same directory the directive expands to:

module File : sig
#line 1 "B0/file.mli"
(* contents of B0/file.mli *)
end = struct
#line 1 "B0/file.ml"
(* contents of B0/file.ml *)
end