Skip to content

Latest commit

 

History

History
457 lines (370 loc) · 11.5 KB

Build-system-support.adoc

File metadata and controls

457 lines (370 loc) · 11.5 KB

Build system support

The BuckleScript compilation model is similar to OCaml native compiler. If b.ml depends on a.ml, you have to compile a.ml and a.mli first.

Note

The technical reason is that BuckleScript will generate intermediate files with the extension .cmj which are later used for cross module inlining, arity inference and other information.

BuckleScript build system: bsb

BuckleScript provides a native build tool on top of Google’s ninja-build. It is designed for a fast feedback loop (typically 100ms feedback loop) and works cross platform.

bsb is a schema based build tool. The schema is {schema}[available]. It is strongly recommended to check out the {schema}[schema] after you finish reading the tutorials below.

If your editor supports JSON schema validation and auto-completion like VSCode, below is a configuration to help you get auto-completion and validation for free:

settings.json
{
    "json.schemas": [
        {
            "fileMatch": [
                "bsconfig.json"
            ],
            "url" : "file:///path/to/bucklescript/docs/docson/build-schema.json" // (1)
        }
    ],
    // ....
}

The build system is installed as bsb.exe in bs-platform/bin/bsb.exe. Due to a known issue in npm, we create a JS wrapper (which is accessible in .bin too) so the user can call either bsb (slightly higher latency but symlinked into .bin) or bsb.exe

Here is a minimal configuration:

bsconfig.json
{
  "name": "test", // package name, required (1)
  "sources": "src" // (2)
}
  1. It is an extension to JSON with comment support

  2. Here we did not list files, so all .ml, .mli, .re, .rei will be considered as source files

The entry point is bsb. It will check if there is already build.ninja in the build directory, and if not or it needs to be regenerated it will generate the file build.ninja and delegate the hard work to ninja.

The directory layout (after building) would be

.
├── lib
│   ├── bs
│   │   ├── src
│   │   └── test
│   ├── js
│   │   ├── src
│   │   └── test
│   ├── amdjs // (1)
│   │   ├── src
│   │   └── test
│   ├── es6  // (2)
│   │   ├── src
│   │   └── test
│   └── ocaml
├── scripts
├── src
└── test
  1. Will generate AMDJS modules if flags are turned on

  2. Will generate ES6 modules if flags are turned on

We wrap bsb.exe as bsb so that it will work across different platforms.

Watch mode
bsb // (1)
bsb -w // (2)
  1. Do the plain build

  2. Do the plain build and watch

Build with other BuckleScript dependencies

List your dependency in bs-dependencies and install it via npm install as below:

bsconfig.json
{
    "name": "bs-string",
    "version": "0.1.3",
    "bs-dependencies": [
        "bs-mocha" // (1)
    ],
    "sources": [
       .. .
    ],
    "generate-merlin" : false // (2)
}
  1. Yet another BuckleScript dependency

  2. If true (default) bsb will generate merlin file for you

package.json
{
 "dependencies": {
    "bs-mocha": "0.1.5"
    },
  ...
}

After your npm install,

bsb -clean-world // (1)
bsb -make-world // (2)
bsb -w // (3)
  1. Clean the binary artifact of current build and your dependency

  2. Build dependencies and lib itself

  3. Start watching the project and whenever your changes are made, bsb will rebuild incrementally

You can also streamline the three commands as below:

bsb -clean-world -make-world -w

Mark your directory as dev only

Note sometimes, you have directories which are just tests that you don’t need your dependency to build. In that case you can mark it as dev only:

bsconfig.json
{
        "sources" : {
                "dir" : "test",
                "type" : "dev" // (1)
        }
}
  1. directory test is in dev mode, it will not be built when used as a dependency

A real world example of using bsb

Below is a json configuration for the bucklescript-tea: the Elm artchitecture in BuckleScript

bsconfig.json
{
  "name": "bucklescript-tea",
  "version": "0.1.3",
  "sources": [
   "src", // (1)
    {
      "dir": "test",
      "type": "dev" // (2)
    }
  ]
}
  1. Source directory, by default it will export all units of this directory to users.

  2. Dev directory, which will only be useful for developers of this project.

package.json
{
  "name": "bucklescript-tea",
  "version": "0.1.3",
  "description": "TEA for Bucklescript",
  "scripts": {
    "build": "bsb",
    "watch": "bsb -w",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "peerDependencies": {
    "bs-platform": "^1.7.0" // (1)
  }
}
  1. Here we list bs-platform as a peer dependency so that different repos shares the same compiler.

Now, if we have a repo bucklescript-have-tea which depends on bucklescript-tea, its configurations are as below:

bsconfig.json
{
    "name" : "bucklescript-have-tea",
    "sources" : "src",
    "bs-dependencies": [
      "bucklescript-tea"
    ]
}
package.json
{
    "name" : "bucklescript-have-tea",
    "version" : "0.1.0",
    "dependencies" : { "bucklescript-tea" : "^0.1.2" }, // (1)
    "peerDependencies" : { "bs-platform" : "^1.7.0" } //(2)
}
  1. List bucklescript-tea as dependency

  2. List bs-platform as peer dependency

Suppose you are in bucklescript-have-tea top directory,

npm install // (1)
npm install bs-platform (2)
./node_modules/.bin/bsb -clean-world -make-world -w (3)
  1. Install the dependencies

  2. Install peer dependencies

  3. On Windows, it would be .\node_modules\.bin\bsb -clean-world -make-world -w

You can also change the package-specs to have another module format, for example, tweak your bsconfig.json:

{
  ... ,
  "package-specs" : ["amdjs", "commonjs"],
  ...
}

Rerun the command

bsb -clean-world -make-world

You will get both commonjs and amdjs support. In the end, we suggest you check out the schema and enjoy the build!

namespace support (@since 1.9.0)

OCaml treats file names as modules, so that users can only have unique file names in a project, BuckleScript solves the problem by scoping all modules by package.

Below is the bsconfig.json for liba, libb (they share the same configuration in this example)

{
  "name": "liba",
  "version": "0.1.0",
  "sources": "src",
  "namespace": true
}

Now you have a library to use them

{
  "name": "namespace",
  "version": "0.1.0",
  "sources": "src",
  "bs-dependencies": [
    "liba",
    "libb"
  ]
}

Since liba and libb are configured using namespace, to use them in source code, it would be like

let v =
    (Liba.Demo.v + Libb.Demo.v)
Note

In the same package, everything works the same whether it uses namespace or not, it only affects people who use your library

In source build support (@since 1.9.0)

When user has an existing JS project, it makes sense to output the JS file in the same directory as vanilla JS, so the schema added a key called in-source so that the JS file is generated next to ML file.

Example:

{
  ...
  "package-specs" : [{
    "module": "commonjs",
    "in-source": true
  }]
}

Build using Make

Warning

bsb is the officially recommended build system. This section is included here only for people who are curious about how the build works.

BuckleScript distribution has bsdep.exe which has the same interface as ocamldep

Here is a simple Makefile to get started:

Makefile
OCAMLC=bsc.exe # (1)
OCAMLDEP=bsdep.exe # (2)
SOURCE_LIST := src_a src_b
SOURCE_MLI = $(addsuffix .mli, $(SOURCE_LIST))
SOURCE_ML  = $(addsuffix .ml, $(SOURCE_LIST))
TARGETS := $(addsuffix .cmj, $(SOURCE_LIST))
INCLUDES=
all: $(TARGETS)
.mli:.cmi
        $(OCAMLC) $(INCLUDES) $(COMPFLAGS) -c $<
.ml:.cmj:
        $(OCAMLC) $(INCLUDES) $(COMPFLAGS) -c $<
-include .depend
depend:
        $(OCAMLDEP) $(INCLUDES) $(SOURCE_ML) $(SOURCE_MLI) > .depend
  1. bsc.exe is the BuckleScript compiler

  2. ocamldep executable is part of the OCaml compiler installation

In theory, people need run make depend && make all. make depend will calculate dependencies while make all will do the job.

However in practice, people used to a file watch service, such as watchman for example, will need the JSON configuration:

build.json
[
    "trigger", ".", {
        "name": "build",
        "expression": ["pcre", "(\\.(ml|mll|mly|mli|sh|sh)$|Makefile)"], // (1)
        "command": ["./build.sh"],
        "append_files" : true
    }
]
  1. whenever such files are changed, it will trigger the command field to be run

build.sh
make -r -j8 all (1)
make depend // (2)
  1. build

  2. update the dependency

Now in your working directory, type watchman -j < build.json and enjoy the lightning build speed.

Customize rules (generators support, @since 1.7.4)

It is quite common for programmers to use some preprocessors to generate some boilerplate code during developement.

Note preprocessors can be classified in two categories: one is system-dependent which must be delayed until running on the target machines and the other is system-independent (such as lex, yacc, m4, re2c, etc.) which could be executed anytime.

BuckleScript has built in support for conditional compilation for the second category. Since it is system-independent, we ask users to always generate such code and check it in before shipping, which would cut the dependencies for end users.

A typical example would be like this

Bsb using ocamlyacc
{
    "generators" : [
        { "name" : "ocamlyacc" ,
          "command" : "ocamlyacc $in" }
    ],
    ...,
    "sources" : {
        "dir" : "src",
        "generators" : [
            {
                "name" : "ocamlyacc",
                "edge" : ["test.ml", "test.mli", ":", "test.mly"]
            }
        ]
    }
}

Note ocamlyacc will generate test.ml and test.mli in the same directory with test.mly. The developer should check in the generated file since then users would not need run ocamlyacc again. This would apply to menhir as well.

When a developer is working on their current project, bsb will track the dependencies between test.ml and test.mly properly. When released as a package, bsb will cut such a dependency, so that users will only need the generated test.ml. To help test such behavior, developers can set it manually:

{
    ...,
    "cut-generators" : true
}

With this setting bsb will not regenerate test.ml whenever test.mly changes.