Warning
This repository has been moved to Codeberg: OCaml-Buck-2-Examples
This contains documentation and examples on how to use Buck 2 to build OCaml projects.
- Installation
- Buck 2 Usage with OCaml
- Using Buck 2 with OCaml-LSP or Merlin
- Examples
- Other Buck 2 with OCaml Resources
- Questions and Answers
- Buck 2 Examples for other Languages
- Contributions
- License
You need the following:
- Buck 2: Buck2 Official Website
- Facebook's/Meta's Python scripts to handle dependencies on Opam packages and generate Buck 2 configuration files: ocaml-scripts at GitHub.
- Python 3, Python 3.8 or newer.
Install Buck 2 like documented at Buck 2 - Getting Started.
Copy the scripts meta2json.py
, rules.py
and dromedary.py
from the ocaml-scripts
repository to some place you can use it in your project(s), no "real" installation needed. Warning: all three must be in the same directory.
To actually clone the Buck 2 prelude into the prelude
subdirectories of this repository, you have to update them:
- Run
git submodule update --init
in the root directory ./.
Buck 2 has an integrated (Starlark) LSP, which can be started using buck2 lsp
in the root directory of a Buck 2 project. To use this to help with writing BUCK
(Starlark) files, you need a plugin for your editor of choice:
- VS Code/VSCodium: Buck2 LSP GitHub: Buck2 LSP - GitHub
To generate a new OCaml project with Buck 2:
- Generate the Buck 2 project structure using
buck2 init
- Generate a new Opam switch (a sandbox) and a Buck 2 configuration file for the Opam packages, using
dromedary.py
and a dromedary configuration file from Meta'socaml-scripts
. - Edit the Buck 2 configuration files for the build targets
To use Buck 2 for an existing OCaml project:
- Generate the Buck 2 project structure using
buck2 init
- Generate a Buck 2 configuration file for the Opam packages of an existing Opam switch (sandbox), using
dromedary.py
from Meta'socaml-scripts
. - Edit the Buck 2 configuration files for the build targets
The following steps are needed to setup a Buck 2 project. These are the same for new and already existing OCaml projects.
-
buck2 init --git
to generate all needed files and directories and add the Buck 2 prelude directory as a git submodule, so you are able to update it using git. The Prelude of Buck 2 contains the build rules for every language, for example the ones for OCaml. -
Building the generated default target using
buck2 build //..
should work without errors now:% buck2 build //... Build ID: a125ba60-835a-4afb-813f-cede508c833f Jobs completed: 6. Time elapsed: 0.0s. Cache hits: 0%. Commands: 1 (cached: 0, remote: 0, local: 1) BUILD SUCCEEDED
-
Set the execution platform in
./.buckconfig
to the default one of the prelude.[build] execution_platforms = prelude//platforms:default
-
Add the needed toolchains to
./toolchains/BUCK
:. edit the file./toolchains/BUCK
as follows, to include the Python and C++ toolchains, which are always needed, and the OCaml toolchain:
load("@prelude//toolchains:cxx.bzl", "system_cxx_toolchain")
load("@prelude//toolchains:genrule.bzl", "system_genrule_toolchain")
load("@prelude//toolchains:ocaml.bzl", "system_ocaml_toolchain")
load("@prelude//toolchains:python.bzl", "system_python_bootstrap_toolchain")
system_cxx_toolchain(
name = "cxx",
visibility = ["PUBLIC"],
)
system_genrule_toolchain(
name = "genrule",
visibility = ["PUBLIC"],
)
system_ocaml_toolchain(
name = "ocaml",
visibility = ["PUBLIC"],
)
system_python_bootstrap_toolchain(
name = "python_bootstrap",
visibility = ["PUBLIC"],
)
- Generate a subdirectory
./third-party
in the prohect root directory to hold the information about third-party packages. The name can be any you like, butthird-party
is the canonical one, that's also used in all examples. If you've chosen another one, substitutethird-party
with your directory name in all of the documentation.
To generate a new switch and install Opam packages in this switch, do the following:
-
Edit the JSON configuration file
./third-party/dromedary.json
. -
Fields, these are the arguments you would pass to
opam create
:name
, a string: the path to the Opam sandbox or the name of the global Opam switch to create.compiler
, a string: the name of the compiler to use.packages
, a list of strings: the name and optionally versions of the packages to install in the Opam switch.
-
The only mandatory field is
packages
, the default forname
is./
and the default forcompiler
isocaml-variants
. -
Example file
./third-party/dromedary.json
:{ "name": "./", "compiler": "ocaml-variants", "packages": [ "menhirLib", "sedlex=3.2", "alcotest>=1.7.0" ] }
results in the following opam command in the project root:
opam switch create ./third-party/ ocaml-variants 'menhirLib' 'sedlex=3.2' 'alcotest>=1.7.0'
-
Run
python3 dromedary.py -o ./third-party/BUCK ./third-party/dromedary.json
. Instead ofdromedary.py
you need to use the actual path to the Python script. Substitutethird-party
with the name you have chosen for your subdirectory. -
The file
./third-party/BUCK
should now have been created. -
The symlink
./third-party/opam
should link to the generated Opam switch. -
Running
buck2 targets //third-party:
(or the name of your subdirectory instead ofthird-party
) should now list all installed Opam packages of the switch.
% buck2 targets //third-party:
Build ID: ed8ad382-6848-44c7-842c-a537fa916411
Jobs completed: 4. Time elapsed: 0.3s.
root//third-party:alcotest
root//third-party:alcotest.alcotest-plugin
root//third-party:alcotest.engine
root//third-party:alcotest.engine.alcotest_engine-plugin
root//third-party:alcotest.runtime.js
root//third-party:alcotest.stdlib_ext
root//third-party:alcotest.stdlib_ext.alcotest_stdlib_ext-plugin
root//third-party:astring
root//third-party:astring.astring-plugin
root//third-party:astring.top
root//third-party:astring.top.astring_top-plugin
root//third-party:base
root//third-party:base.base-plugin
...
To use all opam packages of an existing Opam switch (or sandbox) with name or directory OPAM_SWITCH
, do the following:
- Run
python3 dromedary.py --switch OPAM_SWITCH -o ./third-party/BUCK
. Instead ofdromedary.py
you need to use the actual path to the Python script. Substitutethird-party
with the name you have chosen for your subdirectory. - The file
./third-party/BUCK
should now have been created. - The symlink
./third-party/opam
should link to the path of the Opam switch. - Running
buck2 targets //third-party:
(or the name of your subdirectory instead ofthird-party
) should now list all installed Opam packages of the switch.
% buck2 targets //third-party:
Build ID: ed8ad382-6848-44c7-842c-a537fa916411
Jobs completed: 4. Time elapsed: 0.3s.
root//third-party:alcotest
root//third-party:alcotest.alcotest-plugin
root//third-party:alcotest.engine
root//third-party:alcotest.engine.alcotest_engine-plugin
root//third-party:alcotest.runtime.js
root//third-party:alcotest.stdlib_ext
root//third-party:alcotest.stdlib_ext.alcotest_stdlib_ext-plugin
root//third-party:astring
root//third-party:astring.astring-plugin
root//third-party:astring.top
root//third-party:astring.top.astring_top-plugin
root//third-party:base
root//third-party:base.base-plugin
...
File ./lib/BUCK
This is the easiest case, just compile all ml
files into a library named my_project
. deps
would hold library names the library depends on.
ocaml_library(
name = "my_project",
srcs = glob(["./*.ml"]),
deps = [],
visibility = ["PUBLIC"],
) if not host_info().os.is_windows else None
But this library does not contain the files in a single module. For example the module Bar
of the file bar.ml
is named Bar
, not My_project.Bar
.
To have all the files of the library contained in a parent module My_project
, you need to do a mapping:
File ./lib/my_project.mli
(* for file foo.ml *)
module Foo = Foo
(* for file bar.ml *)
module Bar = Bar
File ./lib/my_project.ml
(* for file foo.ml *)
module Foo = Foo
(* for file bar.ml *)
module Bar = Bar
File ./lib/BUCK
Give a name
to the file my_project.mli
, so we can use that in other rules.
export_file(
name = "my_project.mli",
src = "my_project.mli",
visibility = [
":my_project",
":my_project__",
],
)
Add the mapping as an extra target.
# buildifier: disable=no-effect
ocaml_library(
name = "my_project__",
srcs = [
"my_project.ml",
":my_project.mli",
],
compiler_flags = [
"-no-alias-deps",
],
visibility = [":my_project"],
) if not host_info().os.is_windows else None
Compiler the library, using the mapping. ocamldep_flags
are flags to be passed to ocamldep
.
# buildifier: disable=no-effect
ocaml_library(
name = "my_project",
srcs = ["./foo.ml", "./bar.ml"],
compiler_flags = [
"-no-alias-deps",
"-open",
"My_project",
],
ocamldep_flags = [
"-open",
"My_project",
"-map",
"$(location :my_project.mli)",
],
deps = [":my_project__"],
visibility = ["PUBLIC"],
) if not host_info().os.is_windows else None
File ./bin/BUCK
This binary named my_project
is contained in one source file, ./main.ml
and uses the "internal" library my_project
located in the subdirectory lib
of the project root.
ocaml_binary(
name = "my_project",
srcs = ["./main.ml"],
deps = [
"//lib:my_project",
],
visibility = ["PUBLIC"],
) if not host_info().os.is_windows else None
File ./test/BUCK
This test binary named my_project
is contained in one source file, ./my_project_test.ml
and uses the "internal" library my_project
located in the subdirectory lib
of the project root and the libraries alcotest
and qcheck-alcotest
from the third-party
subdirectory.
The binary is missing some libraries when linking, we have to pass -cclib -lunixbyt -cclib -lunixnat
to the OCaml compiler.
ocaml_binary(
name = "my_project",
srcs = ["./my_project_test.ml"],
deps = [
"//lib:my_project",
"//third-party:alcotest",
"//third-party:qcheck-alcotest"
],
compiler_flags = [
"-cclib",
"-lunixbyt",
"-cclib",
"-lunixnat",
],
visibility = ["PUBLIC"],
) if not host_info().os.is_windows else None
To compile a library using a PPX library, we have to first generate an executable from the PPX. Than use this executable to parse the the source file and generate the actual OCaml source file to be compiled by the OCaml compiler.
File ./lib/driver.ml
This file (yes, that is all) is compiled to an executable, that later parses the source files containing the PPX syntax.
let () = Ppxlib.Driver.standalone ()
File ./lib/BUCK
This rule generates the executable named ppx
from the ./lib/driver.ml
source file and the PPX libraries.
ocaml_binary(
name = "ppx",
srcs = ["./driver.ml"],
compiler_flags = [
"-linkall",
],
deps = [
"//third-party:sedlex",
"//third-party:sedlex.ppx",
],
) if not host_info().os.is_windows else None
File ./lib/BUCK
Now we use the PPX executable named ppx
from above to parse the PPX syntax. It is passed to the OCaml compiler using -ppx $(exe_target :ppx) --as-ppx
.
ocaml_library(
name = "my_project",
srcs = ["./ast.ml", "./interpreter.ml", "./lexer_ex.ml"],
compiler_flags = [
"-no-alias-deps",
"-ppx",
"$(exe_target :ppx) --as-ppx",
],
deps = ["//third-party:sedlex",
"//third-party:sedlex.ppx"],
visibility = ["PUBLIC"],
) if not host_info().os.is_windows else None
To be able to use inline tests, we have to also compile a test runner executable for these to be called. This needs an extra ocaml_binary
target to run the inline tests.
File ./lib/BUCK
We have to generate an inline test runner executable and link all libraries containing inline tests and the Alcotest inline runner library ppx_inline_alcotest.runner
with it.
ocaml_binary(
name = "inline_runner",
srcs = ["./inline_runner.ml"],
compiler_flags = [
"-linkall",
"-cclib",
"-lunixbyt",
"-cclib",
"-lunixnat",
],
deps = [":inline_test_runners",
"//third-party:ppx_inline_alcotest.runner",],
visibility = ["PUBLIC"],
) if not host_info().os.is_windows else None
The processing of the library's sources works the same as above, with a PPX driver executable :ppx
.
File ./lib/inline_runner.ml
The test runner executable's source.
let () = Ppx_inline_alcotest_runner.run ()
File ./test/BUCK
We have to generate an inline test runner executable and link all libraries containing inline tests and the Alcotest inline runner library ppx_inline_alcotest.runner
with it.
ocaml_binary( name = "ppx", srcs = ["./driver.ml"], compiler_flags = [ "-linkall", ], deps = ["//third-party:ppx_inline_alcotest", ], ) if not host_info().os.is_windows else None
File ./test/inline_tests.ml
The test runner executable's source, add the stanza let () = Ppx_inline_alcotest_runner.run ()
to run the inline tests.
(* ... *)
let%test "1 is 1" = Alcotest.(check int) "same ints" 1 1
let () = Ppx_inline_alcotest_runner.run ()
The processing of the executable's sources works the same as above, with a PPX driver executable :ppx
and by adding the PPX driver to the test executable's config:
ocaml_binary(
name = "inline_test_runner",
srcs = [YOUR_SOURCES],
deps = [
"//lib:LIBRARY_TO_TEST",
"//third-party:ppx_inline_alcotest",
"//third-party:ppx_inline_alcotest.runner",
"//third-party:alcotest"
],
compiler_flags = [
"-cclib",
"-lunixbyt",
"-cclib",
"-lunixnat",
"-ppx",
"$(exe_target :ppx) --as-ppx",
],
visibility = ["PUBLIC"],
) if not host_info().os.is_windows else None
You can use aliases for targets, for example in subdirectories.
If you have 3 subdirectories, bin
, lib
and test
, and the target in each subdirectory has the name my_project
defined in its BUCK
file, you can define the following aliases:
File ./BUCK
in the project root defines the aliases:
alias(
name="bin",
actual="//bin:my_project",
visibility=["PUBLIC"],
)
alias(
name="lib",
actual="//lib:my_project",
visibility=["PUBLIC"],
)
alias(
name="test",
actual="//test:my_project",
visibility=["PUBLIC"],
)
So, instead of buck2 run //test:my_project
you can now use buck2 run //:test
See Examples.
You need to generate a .merlin
file containing the paths to the source and build directories and the list of Opam packages (but using ocamlfind
names!), see Merlin - Project Configuration
Example:
S bin/**
S lib/**
S test/**
B ./buck-out/v2/gen/root/*/bin/__ppx_usage_example__/_nativeobj_
B ./buck-out/v2/gen/root/*/lib/__ppx__/_nativeobj_
B ./buck-out/v2/gen/root/*/lib/__ppx_usage_example__/_nativeobj_
B ./buck-out/v2/gen/root/*/lib/__ppx_usage_example__/_nativeobj_/_native_gen_
B ./buck-out/v2/gen/root/*/lib/__ppx_usage_example____/_nativeobj_
B ./buck-out/v2/gen/root/*/test/__ppx_usage_example__/_nativeobj_
PKG sedlex menhirLib qcheck-alcotest alcotest sedlex.ppx
You can generate the B
stanzas using find
, the cmi
files are located at _nativeobj_
directories in buck-out
.
dirname $(find ./buck-out -name "*.cmi") | sort | uniq
To be able to use the .merlin
file with OCaml-LSP, you need to add the command line argument --fallback-read-dot-merlin
to the ocaml-lsp invocation and need the Opam package dot-merlin-reader
installed. OCaml-LSP documentation: Merlin configuration
For VS Code or Codium the setting to add the command line argument is
"ocaml.server.args": [
"--fallback-read-dot-merlin"
],
Each example project directory contains a README.md
file explaining its Buck 2 configuration files.
- ./basic_example/ - a OCaml project which uses Opam package dependencies, but no PPX. Contains a library, an executable that uses the library and tests of the library.
- ./ocamllex_menhir_example/ - a OCaml project which uses OCamlLex and Menhir to generate OCaml code. Contains a library, an executable that uses the library and tests of the library.
- ./ppx_usage_example/ - a OCaml project which uses Sedlex and Menhir as code generators and PPX for Sedlex to show the usage of PPX libraries. Contains a library, an executable that uses the library and tests of the library.
- ./inline_test_runners - a OCaml project which uses PPX inline tests, the library uses Alcotest inline PPX. Contains a library, an executable that uses the library and tests and inline tests of the library.
All of these examples also use Dune configuration files, so you can compare them to the BUCK
files.
In the file ./.github/workflows/test.yml there are working Github actions for MacOS and Linux. The Windows part does not work, as passing the PATH into dromedary.py
(which calls other Python scripts, which call ocamlfind
) fails.
- the official Facebook/Meta examples in the Buck 2 GitHub Repo.
- ICFP 2023, Shayne Fletcher, Neil Mitchell:
No. Buck2 itself works on Windows, but OCaml's build rules do not support Windows so far. But you can change this: see OCaml rules in Buck 2 Prelude at GitHub.
The short answer is "yes". The longer answer is: yes, but you need to manually add changes to dune
files to BUCK
files and vice versa. There does not exist a script (so far) that does this work for you.
If you have to ask, you may not need it ;). It's mostly useful if you use more than one language (more than just OCaml) that you want to build.
Buck2 has OCaml support from Meta itself, not a third party and it is written in Rust, which I use myself, so there is no additional dependency for me on a JVM (which Bazel uses). And, last but not least, Bazel is made by Google, which sometimes (ahem) abruptly stops supporting some of their projects. But Bazel has more mature support as Buck 2 is much younger and less used.
There is Bazel support for OCaml by OBazl, documentation: The OBazl Book. I have never tried Bazel (or the OCaml support), so I cannot say anything about it. Try it out for yourself.
If you are getting an error message like
Error: Files third-party/opam/lib/uutf/uutf.cmxa
and /Users/roland/.opam/5.0.0/lib/ocaml/stdlib.cmxa
make inconsistent assumptions over implementation Stdlib__String
The Opam environment of your shell and the one used by Buck 2 may be off. Set the Opam environment to the one of the right switch using opam switch BUCK2_SWICH_NAME
and call opam env
afterwards. The output of opam switch BUCK2_SWICH_NAME
tells you how. Then clean your project: buck2 clean
and rebuild everything: buck2 build //...
I've also made example projects using C++ with vcpkg as package manager: Cxx-Buck2-vcpkg-Examples and with Conan (the C++ package manager) at Cxx-Buck2-Conan-Examples.
If you want to add tips or tricks on using Buck 2, examples, a link to other examples, blog or forum posts, or found an error, please open an issue or pull request with your changes.
All files in this repository are licensed under the MIT license, see file ./LICENSE.