Getting started
esy provides a single command called esy
that can be invoked inside of any
directory containing a package.json
file. The typical workflow is to cd
into a directory that contains a package.json
file, and then perform
operations on that project.
Install esy
npm install -g esy
If you had installed esy previously:
npm uninstall --global --update esy
Step-by-step tutorial
To understand the benefits and general workflow of writing a program with esy, we put together a simple Step-by-Step guide for you to follow. In this guide we want to write a simple "Hello, World" program. It will just print "Hello, World" to the console and exit afterwards.
If you have questions or you find yourself stuck anywhere, don't hesitate to reach out at one of our community platforms
Let's start with an empty package.json
{}
Every program we may want to write with ReasonML or OCaml needs at least the OCaml compiler as a dependency.
{
"dependencies": {
"ocaml": "4.10.x"
}
}
And our OCaml source file, hello.ml
with
let () = print_endline "Hello, World"
To install the dependencies, run esy
$ esy
This will fetch all of our dependencies (at this point, just ocaml
) and install it in a sandbox exclusively created for this project.
A esy sandbox is like an isolated environment for your project, so everything you install is just installed inside this environment and not globally on your system.
The advantage of a sandbox is that different projects can have different versions of the same dependency installed, which would not be easily possible if they were just installed globally.
In OCaml, we can't just run our code as with for example NodeJS. It first needs to be compiled to an executable with the OCaml compiler we installed previously.
The compiler gets invoked with either the command ocamlopt
(for native compilation) or ocamlc
(for bytecode compilation). You can learn more about the compilers here. In this example, we will prodeed with the native compiler (ocamlopt). The most basic syntax for compiling a file to an executable looks like this: ocamlopt -o <output.exe> <source-file>
.
With this info at hand, our first instinct might be just running ocamlopt -o hello.exe hello.ml
to produce our executable file. But remember, we didn't install ocaml globally, but in our projects sandbox. So we need to somehow run this command inside of this sandbox.
Just like in the Yarn/NPM world, build commands are run with a prefix.
To compile,
esy ocamlopt -o hello.exe hello.ml
If everything worked correctly, running the command above should have output a file called hello.exe. To now execute our program and output "Hello, World", we just need to run:
./hello.exe
If you are following along on a Mac or Linux, it might seem a bit odd, that you can run an .exe file, as these files normally can only be run on Windows systems. This is just for convenience and completely optional.
Using a build system
Compiling your code with ocamlopt or ocamlc works well, when you don't have a lot of dependencies. But as soon as your project gets larger, compiling your code manually becomes very complex, very fast. To help with this, we want to use a build system.
If you are coming from JavaScript, you probably already heard from tools like "Webpack", "Browserify" or "Rollup". These are build systems. They take your code and its dependencies and bundle them together into a single file.
For Reason and OCaml, we use Dune - the community endorsed build system. You give it an entry point and it "bundles" all of your code and its dependencies together into a single executable.
To install dune into our "Hello World" program, we have to add @opam/dune
to the dependencies of our package.json.
"dependencies": {
"ocaml": "4.10.x",
+ "@opam/dune": "*"
}
If you're familiar with Dune, you'd know, that starting from scratch, Dune needs
-
A
dune-project
file describe the project -
A
dune
file to describe the binary being compiled -
An opam file (say
hello.opam
) to so that Dune can assign the project an identifier. -
Create an empty file
hello.opam
. This file assigns the namespacehello
to our package among the set of libraries. -
Create a
dune
file to describe how the program. Mostly, just it's public name and the file creating it(executable
(name hello) ; asking dune to build hello.ml
(public_name hello.exe)) ; name of the binaryWe're now ready to build and distribute the hello world program.
-
Optionally, create a
dune-project
file. If you don't, Dune will create it for you.
(lang dune 2.7)
(name hello)
lang
specifies the configuration language version. Yes! Dune configuration language is versioned which brings extra stability!name
specifies the identifier for the project. This has to be same as the opam file name.
As with the compilation of single program, you might expect to prefix
Dune's build command, dune build
, with esy
and expect things to
work.
esy dune build
You're not wrong.
However, esy's real strength is isolated build environment - read the
package.json
, create an isolated environment, try to build the project
there.
This way, should you happen to forget to include a dependency in your
package.json, the build will break. Other package managers don't guarantee
this! Esy also creates lock files, not optional like in opam
. This
ensures users have the same version of a dependency across machines.
To build projects in isolated build environments, use esy b
prefix.
esy b dune build
You can now find the files Dune usually creates under _build
directory at _esy/default/build
. Run the binary with,
./_esy/default/build/default/hello.exe
You can think of it as ./_esy/{your esy sandbox name}/build/{your dune profile}/hello.exe
Alternatively,
esy b dune exec ./hello.exe
Out of source
It, indeed, was very convenient to have the binary, hello
, created
right next to the source file, hello.ml
. As they'd say, in-source
compilation.
In real world applications, such binaries are rarely run directly. For two reasons,
- They are installed in a global location somewhere else. Like
/usr/local
,/usr/bin
- Running them directly (
$ /path/to/hello.exe
) is rarely reliable as they might depend on a version of library that simply may not be present, or the wrong version.
Just like build environments, it would be nice to our project binaries in a special environment where the binaries and libraries project needs is available in the exact version needed and isolated from what the rest of the machine.
Esy provides just that - we call it the exported environment.
We need to
- Install the binary
- Call this binary from the exported environment.
By install, we mean installing it to a location local to the project, but for all intents and purposes, it behaves like as it it were a global location. A virtualised environment.
To do so, tweak the build command to ask Dune to not just compile, but also to generate some special files that would help in installing the binary.
esy b dune build -p hello
To run the binary in the exported environment, one would have to build and then run the install command in sequence.
esy b dune build -p hello && esy b dune install # Only for illustration. Not a valid command
This would, theoretically, install the binary in a project local sandbox and make it available in the exported environment.
We instead recommend, specifying the build command, dune build -p hello
, in the package.json.
{
+ "esy": {
+ "build": "dune build -p hello"
+ },
"dependencies": {
"ocaml": "4.10.x",
"@opam/dune": "*"
}
}
Esy understands Dune, and the following will not be necessary
"esy": {
"build": "dune build -p hello",
- "install": "dune install"
},
With the final package.json looking like this,
{
"esy": {
"build": "dune build -p hello"
},
"dependencies": {
"ocaml": "4.10.x",
"@opam/dune": "*"
}
}
The command to build the project simply becomes, esy
$ esy
info esy 0.6.10 (using package.json)
info fetching: done
info installing: done
Done: 13/17 (jobs: 1)
$
To check if the built project would install and run fine, instead of a
command like esy b <run install command> && ./path/to/hello.exe
, we
can simply run,
esy x hello.exe
ie. prepending esy x
to the final command the user of the tool expects
it. If the tool is invoked on the deployed machine as $ mytool --option1 --option2
, you can test it during development in it's
exported environment as $ esy x mytool --option1 --option2
.
That's it!
We have a template, hello-reason ready for you to help you quickly get started.
git clone https://github.com/esy-ocaml/hello-reason.git
cd hello-reason
esy
Note: to get intellisense on VSCode, make sure the latest version of
@opam/ocaml-lsp-server
is present in package.json
. OCaml versions
may differ across projects and a single global ocamllsp
binary won't
work across projects. The hello-reason
template takes care of this
already.
Cheat sheet
Install dependencies
Make sure the package.json has the dependencies specified. Then run,
esy install
# or
esy i
# or simply
esy
Build
Make sure the package.json as the build command specified. And then,
esy build
# or
esy b
# or simply
esy
Run compiled executables
Use esy x COMMAND
invocation to run project's built executable as if they are
installed:
esy x Hello
Where is the binary executable?
When a compiled binary is run with esy x Hello
, esy creates a local install sandbox with /bin
, /lib
, /etc
and other directories found globally where binaries are meant to be installed. If you're curious, you could peek into them, running
esy echo '#{self.install}'
and inspect the contents yourself. You'll find the binaries in the bin
directory.
However, they are not meant to be run directly as they could be missing the necessary exported environment - it could be possible that the binary you created needs a dependency during the runtime. esy x <your project binary>
is the recommended way to run them.
Checkout concepts for more information.
Rebuild the project
Hack on project's source code and rebuild the project:
esy
Adding a dependency
esy add <dependency>
This will fetch the sources and copy them in esy's store. Next, run
esy build
This will build the newly downloaded dependency.
Alternatively, add a new entry in the dependencies
(or devDependencies
)
"@reason-native/console: "*"
+ "@reason-native/pastel": "*"
"@reason-native/rely": "*"
And run, esy afterwards.
Other useful commands
It is possible to invoke any command from within the project's sandbox. For example build & run tests with:
esy make test
You can run any COMMAND
inside the project development environment by just
prefixing it with esy
:
esy COMMAND
To shell into the project's development environment:
esy shell
For more options:
esy help