Skip to content

obazl/coswitch

Repository files navigation

coswitch

Bazel-OPAM integration.

Usage

@coswitch only converts existing OPAM switches, so to use it you must install and configure a switch. See below for instructions. You must rerun coswitch whenever you change your switch (by installing or removing packages).

Once you have installed a coswitch, you can express dependencies on OPAM packages by adding bazel_dep directives to your root MODULE.bazel file; for example:

bazel_dep(name = "alcotest",        version = "0.0.0")
Important
Version "0.0.0" is a dummy bzlmod version id; the dependency will resolve to whatever version is installed in OPAM.
Warning
If you add bazel_dep directives before you install the coswitch, you will run into a bootstrapping problem. Bazel will not be able to run or build any target because it will not be able resolve the bazel_deps. In particular it will not be able to run the bazel run @coswitch//new command needed to create the coswitch that will enable bazel_dep resolution. To avoid this you can install the coswitch executable on your PATH so you can run it independent of Bazel. See instructions below.

Configuration

You can run @coswitch from any directory equipped with the following files: .bazelrc, MODULE.bazel, and WORKSPACE.bazel. If you install it locally, as described below, you can run it independent of Bazel.

.bazelrc
## common --enable_bzlmod  # enabled by default for Bazel 7+                    (1)
try-import %workspace%/.config/coswitch_registry.bazelrc                        (2)
common --registry=https://raw.githubusercontent.com/obazl/registry/main/        (3)
common --registry=https://bcr.bazel.build                                       (4)
try-import .config/user.bazelrc                                                 (5)
  1. The common prefix on these lines directs Bazel to use these directives for all commands (build, test, run, query).

  2. Imported file must contain --registry=…​ for your coswitch; see below

  3. @coswitch is not yet available in the Bazel Central Repository (BRC)

  4. URL for the BCR. Bazel automatically uses it if no --registry directive is used; otherwise it must be explicitly required, as here.

  5. Optional. Will be ignored if .config/user.bazelrc does not exist. Do not put this file under version control. This allows each user to customize builds by adding Bazel directives. Any file name can be used; .config/user.bazelrc is just an OBazl convention.

.config/coswitch_registry.bazelrc
common --registry=file:///path/to/coswitch/registry      (1)
  1. Path created by coswitch; for example, /home/<uid>/.opam/5.1.1/share/obazl

MODULE.bazel
module(
    name = "mymodule",                               (1)
    version = "0.0.0",
    compatibility_level = 0,
    bazel_compatibility = [">=7.0.0"]                (2)
)
bazel_dep(name = "coswitch", version = "x.y.z")      (3)
  1. A common convention is to use the same name for the module and the basename of the root directory, but this is not a requirement.

  2. The least Bazel version that supports Bazel modules (?)

  3. See the Releases page for the latest version identifier.

WORKSPACE.bazel
#                                (1)
  1. With bzlmod enabled this file will be ignored but it must exist. This constraint will be removed in a future version of Bazel.

Execution

$ bazel run @coswitch//new
INFO: Analyzed target @coswitch~2.3.0//new:new (7 packages lo
aded, 66 targets configured).
INFO: Found 1 target...                                            (1)
Target @coswitch~2.3.0//new:new up-to-date:
  .bazel/bin/external/coswitch~2.3.0/new/new
INFO: Elapsed time: 0.080s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: .bazel/bin/external/coswitch~2.3.0/new/new
Switch name:      4.14.0                                       (2)
Compiler version: 4.14.0
Switch prefix:    $HOME/.opam/4.14.0                           (3)
rules_ocaml version: 2.1.0                                     (3)
Coswitch:         $HOME/.opam/4.14.0/share/obazl               (3)
Registry:         $HOME/.opam/4.14.0/share/obazl               (3)
  1. INFO messages are printed by Bazel

  2. By default coswitch is created for current switch; override with -s or --switch

  3. Printed by @coswitch//new (with $HOME expanded)

By default each coswitch is installed in the corresponding switch, in $OPAM_SWITCH_PREFIX/share/obazl. To instead install to $XDG_DATA_HOME/obazl, pass ` — -x` or ` — --xdg`.

You can ask @coswitch//new to print more information by passing -v one or more times; such arguments must be passed after --; for example:

$ bazel run @coswitch//new -- -v

To silence messages from @coswitch//new, pass -q or --quiet.

To silence messages from Bazel you can use these directives (documented at Commands and Options and Command-Line Reference), which may be passed on the command line (NB: before --) or added to .config/user.bazelrc:

--noshow_progress
--noshow_loading_progress
--show_result=0
--ui_event_filters=-<event>,-<event>   (1)
  1. where <event> is one of: fatal, error, warning, info, progress, debug, start, finish, subcommand, stdout, stderr, pass, fail, timeout, cancelled or depchecker

--registry directives

Once you have configured a coswitch, you must tell Bazel to use it by passing a --registry= directive pointing to the coswitch. For example,

--registry=file:///home/<uid>/.opam/5.1.0/share/obazl

You can pass this on the command line, but it will be more convenient to put it in a bazelrc file. One strategy is to put it in .config/coswitch_registry.bazelrc, and then add this to .bazelrc (as noted above):

try-import %workspace%/.config/coswitch_registry.bazelrc
common --registry=https://raw.githubusercontent.com/obazl/registry/main/
common --registry=https://bcr.bazel.build
Important
Order matters. The coswitch --registry directive should be first, and the directive for the BCR must come last.

If you work with multiple OPAM switches, you will need to create a coswitch for each; then to change to use a different coswitch, edit your --registry directive.

Note
Use of coswitches in this manner is independent of the "current" OPAM switch.

For better integration with OPAM and the shell, you can use a shell script to wrap the bazel invocation. When you install coswitch, a sample script is installed at $XDG_DATA_HOME/obazl/templates/bazel_wrapper.sh. Copy this file to tools/bazel (it must be an executable named bazel). Now when you run a bazel command, control will be passed to this script, which runs opam switch show to discover the current switch, and then constructs the appropriate --registry directive and adds it to the bazel command.

Build target labels

Once @coswitch//new has done its work, the packages in the switch are available to Bazel build programs as standard Bazel labels. The labels are constructed according to a simple schema: package p becomes @p//lib/p; package p.q.r becomes @p//lib/q/r. Executables are labelled @p//bin/p; they are also accesible as @ocaml//bin:p.

Some examples:

OPAM Bazel

ounit2

@ounit2//lib/ounit2

mtime

@mtime//lib/mtime

mtime.clock.os

@mtime//lib/clock/os

yojson

@yojson//lib/yojson

The @ocaml module

Most (all?) OPAM packages contain one META file in their root directories. The standard compiler distributions are a little different. They contain a small number of "distrib-packages":

  • compiler-libs

  • dynlink

  • ocamldoc

  • runtime_events (>=5.0.0)

  • stdlib

  • str

  • threads

  • unix

These are packages (so they can be listed as dependencies), but they are included in the compiler distributions rather than as free-standing OPAM/findlib packages.

Special case: num. Implemented as a free-standing pkg, but installs its files to the standard lib dir (lib/ocaml).

Prior to version 5.0.0 the distributions contained no META files for these packages within the ocaml package. Instead, the distrib-packages were expressed as OPAM top-level "virtual" packages with redirection to artifacts in lib/ocaml. They would be installed by OPAM when the compiler was installed.

For example, the dynlink package for 4.14.0 is represented by <opam-root>/4.14.0/lib/dynlink, which contains only a META file indicating that the dynlink archive files are found in <opam-root>/4.14.0/ocaml

Starting with 5.0.0 the distib-packages are expressed by eight nested META files:

lib/ocaml/compiler-libs/META
lib/ocaml/dynlink/META
lib/ocaml/ocamldoc/META
lib/ocaml/runtime_events/META
lib/ocaml/stdlib/META
lib/ocaml/str/META
lib/ocaml/threads/META
lib/ocaml/unix/META

These packages are no longer represented by toplevel subdirectories within the switch’s lib subdirectory.

@coswitch//new creates a Bazel package for each of these within the @ocaml module. For example, @ocaml//lib/dynlink. For compatibility it also creates a Bazel module for each; for example, @dynlink//lib/dynlink. The build targets in these packages are aliased to those in the @ocaml module:

@dynlink//lib/dynlink => @ocaml//lib/dynlink

@coswitch//new has special logic for translating the compiler distribution itself.

Bazel pkg

Imports (by symlinks to <switch-prefix>)

@ocaml//bin

bin

@ocaml//lib/compiler-libs

lib/ocaml/compiler-libs

@ocaml//lib/compiler-libs:bytecomp

lib/ocaml/compiler-libs/ocamlbytecomp.cmx[a]

@ocaml//lib/compiler-libs:common

alias to @ocaml//lib/compiler-libs:common

@ocaml//lib/compiler-libs:optcomp

alias to @ocaml//lib/compiler-libs:optcomp

@ocaml//lib/compiler-libs:toplevel

alias to @ocaml//lib/compiler-libs:toplevel

@ocaml//lib/dynlink

lib/ocaml/dynlink

@ocaml//lib/ocamldoc

lib/ocaml/ocamldoc

@ocaml//lib/runtime

lib/ocaml - stdlib, std_exit etc.

@ocaml//lib/sdk

C sdk headers lib/ocaml/caml

@ocaml//lib/str

lib/ocaml

@ocaml//lib/stublibs

lib/ocaml

@ocaml//lib/threads

lib/ocaml/threads/threads.cmx[a]

@ocaml// unix

lib/ocaml

@ocaml//platform

OBazl-specific helper pkg

@ocaml//runtime

OBazl-specific helper pkg

@ocaml//toolchain

OBazl-specific helper pkg

@ocaml//version

OBazl-specific helper pkg

Installation

$ bazel run @coswitch//install -c opt

This will install an optimized build of the coswitch executable to $HOME/.local/bin/coswitch; it will also install some template files to $XDG_DATA_HOME/obazl/templates.

You can install the executable to a different directory by passing ` — -b /path/to/bindir`.

Once you have installed coswitch on your local system you can remove its bazel_dep from your MODULE.bazel file, and run $ coswitch.

Troubleshooting

Misconfigured repositories may result in a message like the following:

ERROR: Error computing the main repository mapping: in module dependency chain <root> -> unity@2.5.2.bzl.1: module not found in registries: unity@2.5.2.bzl.1

How It Works

@coswitch converts META files in the OPAM switch to BUILD.bazel files in the Bazel "coswitch". It also generates bzlmod registry records.

By default, both the coswitch and the registry are created in $OPAM_SWITCH_PREFIX/share/obazl.

If you pass -- --xdg, they are installed to $XDG_DATA_HOME/obazl:

$XDG_DATA_HOME/obazl/opam/<switch name>
$XDG_DATA_HOME/obazl/registry/<switch name>

Coswitch

A coswitch is essentially a mirror of a switch. Symlinks are used to integrate the the two.

For example, the coswitch record for package yojson of switch fiveone (as describe below in Creating an OPAM switch) looks like this:

~ $ tree .opam/fiveone/share/obazl/lib/yojson
.opam/fiveone/share/obazl/lib/yojson
├── MODULE.bazel
├── WORKSPACE.bazel
├── bin
│   ├── BUILD.bazel
│   └── ydump -> $HOME/.opam/fiveone/bin/ydump
└── lib
    └── yojson
        ├── BUILD.bazel
        ├── META -> $HOME/.opam/fiveone/lib/yojson/META
        ├── dune-package -> $HOME/.opam/fiveone/lib/yojson/dune-package
        ├── opam -> $HOME/.opam/fiveone/lib/yojson/opam
        ├── yojson.a -> $HOME/.opam/fiveone/lib/yojson/yojson.a
        ├── yojson.cma -> $HOME/.opam/fiveone/lib/yojson/yojson.cma
        ├── yojson.cmi -> $HOME/.opam/fiveone/lib/yojson/yojson.cmi
        ├── yojson.cmt -> $HOME/.opam/fiveone/lib/yojson/yojson.cmt
        ├── yojson.cmti -> $HOME/.opam/fiveone/lib/yojson/yojson.cmti
        ├── yojson.cmx -> $HOME/.opam/fiveone/lib/yojson/yojson.cmx
        ├── yojson.cmxa -> $HOME/.opam/fiveone/lib/yojson/yojson.cmxa
        ├── yojson.cmxs -> $HOME/.opam/fiveone/lib/yojson/yojson.cmxs
        ├── yojson.ml -> $HOME/.opam/fiveone/lib/yojson/yojson.ml
        └── yojson.mli -> $HOME/.opam/fiveone/lib/yojson/yojson.mli

If installed with ` — --xdg`:

~ $ tree .local/share/obazl/opam/fiveone/lib/yojson
.local/share/obazl/opam/fiveone/lib/yojson
├── MODULE.bazel
├── WORKSPACE.bazel
├── bin
│   ├── BUILD.bazel
│   └── ydump -> $HOME/.opam/fiveone/bin/ydump
└── lib
    └── yojson
        ├── BUILD.bazel
        ├── META -> $HOME/.opam/fiveone/lib/yojson/META
        ├── dune-package -> $HOME/.opam/fiveone/lib/yojson/dune-package
        ├── opam -> $HOME/.opam/fiveone/lib/yojson/opam
        ├── yojson.a -> $HOME/.opam/fiveone/lib/yojson/yojson.a
        ├── yojson.cma -> $HOME/.opam/fiveone/lib/yojson/yojson.cma
        ├── yojson.cmi -> $HOME/.opam/fiveone/lib/yojson/yojson.cmi
        ├── yojson.cmt -> $HOME/.opam/fiveone/lib/yojson/yojson.cmt
        ├── yojson.cmti -> $HOME/.opam/fiveone/lib/yojson/yojson.cmti
        ├── yojson.cmx -> $HOME/.opam/fiveone/lib/yojson/yojson.cmx
        ├── yojson.cmxa -> $HOME/.opam/fiveone/lib/yojson/yojson.cmxa
        ├── yojson.cmxs -> $HOME/.opam/fiveone/lib/yojson/yojson.cmxs
        ├── yojson.ml -> $HOME/.opam/fiveone/lib/yojson/yojson.ml
        └── yojson.mli -> $HOME/.opam/fiveone/lib/yojson/yojson.mli

Registry

For each Bazel module in the coswitch (i.e. package in the switch), @coswitch//new also creates a Bazel registry record. The Bazel registry has this structure (documented at Bazel registries):

$ tree -L 1 .opam/fiveone/share/obazl/
.opam/fiveone/share/obazl/
├── bazel_registry.json
├── lib                        (1)
└── modules                    (2)
  1. Contains coswitch package entries as described above

  2. Contains bzlmod registry records

With ` — --xdg`:

$ tree -L 1 .local/share/obazl/registry/fiveone
.local/share/obazl/registry/fiveone
├── bazel_registry.json
└── modules

The bazel_registry.json file contains the path to pkg entries in the OPAM switch:

.opam/fiveone/share/obazl/bazel_registry.json
{
    "mirrors": [],               (1)
    "module_base_path": "lib"     (2)
}
  1. Not used

  2. Relative to cwd; serves as based dir for relative paths in registry records

For XDG installations:

.local/share/obazl/registry/fiveone/bazel_registry.json
{
    "mirrors": [],                                                       (1)
    "module_base_path": "$HOME/.local/share/obazl/opam/fiveone/lib"     (2)
}
  1. Not used

  2. Registry records implicitly reference this base path (in this case, absolute path)

The modules subdirectory contains one record per Bazel module. The record for yojson:

$ tree .local/share/obazl/registry/fiveone/modules/yojson                                                              [fiveone]
.local/share/obazl/registry/fiveone/modules/yojson
├── 0.0.0                               (1)
│   ├── MODULE.bazel
│   └── source.json
└── metadata.json
  1. Coswitch entries always use version 0.0.0, which resolves to whatever version is installed in the OPAM switch. This obviates the need to keep bazel_dep entries in MODULE.bazel files in sync with the OPAM switch.

The source.json file contains the information Bazel needs to find the "source" (i.e. OPAM package) for the Bazel module. In a networked registry, this would be a URL and integrity checksum. But this registry is for local use only, so instead the source.json file looks like this:

{
    "type": "local_path",       (1)
    "path": "yojson"            (2)
}
  1. Tells Bazel to use the module_base_path field of the registry’s bazel_registry.json file to construct the local path to module source.

  2. To be interpreted as the desired subdirectory of module_base_path.

Together, bazel_registry.json and yojson/0.0.0/source.json indicate that the sources for yojson are located at $HOME/.local/share/obazl/opam/fiveone/lib/yojson.

Registry selection

See section above on --registry directives.

Creating an OPAM switch

First update your OPAM installation: $ opam update.

Then create a switch, giving it a name and a compiler version. Synax: opam switch create <name> <compiler id> or just opam switch create <compiler id>. To list available compilers: $ opam switch list-available

Example:

$ opam switch create my511 5.1.1

This should install the switch and make it the "current" switch. Verify:

$ opam switch
Note
To make sure your shell is in sync with OPAM: $ eval $(opam env).

Now install the packages you need:

opam install base bigarray-compat cppo csexp

If you need a lot of packages you can create a simple script:

opaminst.sh
#!/bin/sh
opam install a b c ... etc. ...

Verify installation:

$ opam list