pesy
is a command line tool to assist with your native Reason/OCaml
workflow using the package.json itself.
- Quickly bootstrap a new Reason project
- Configure your builds
npm install -g pesy@next
esy
is driven by a package.json to bring the familiar NPM workflow
to the native world of Reason/OCaml. pesy
takes this further by
adopting NPM conventions to configure the build.esy
's package.json
first approach in creating developer sandboxes brings interesting
possibilities to the table. pesy
leverages on those features to make
native development both convenient and familiar.
pesy
provides
- A bootstrapper script to quickly create a project template
- An alternative JSON syntax around Dune that is NPM like
- Built in tasks that take full advantages of esy's capabilities
pesy
can quickly bootstrap a basic native Reason/OCaml project.
cd my-new-project/
pesy
Say, we need @opam/yojson
and @reason-native/console
in App.re
.
Place App.re
in a folder (say, bin/
?) and add the following to you
package.json.
"buildDirs": {
"bin": {
"imports": [
"Json = require('@opam/yojson')",
"Console = require('@reason-native/console/lib')"
]
}
}
Run esy pesy
(once). Run esy
after that to create builds.
pesy
abstracts library identifiers (in this case yojson
and
console.lib
) in file paths conceptually. Use native dependencies
from OCaml like you did with NPM!
However, pesy
's is truly useful for frameworks that need
a lot dependencies and configuration. Ex: Morph and Revery
pesy --template pesy/template-revery --directory my-new-project
If you are authoring a framework, create a template like
pesy/template-revery
and
run the project on the CI with a setup that pesy
creates for you.
You users get cached CI artifacts that will let can hydrate their
local esy
cache avoid long wait times! See Templates to see how
to create such templates.
Checkout A simple native Reason project with
pesy to get an idea of what developing
with pesy
feels like.
Pesy as accepts package.json as input and producing dune files as output.
+--------+
package.json ---> | pesy | +---> dune files
+--------+
Note that not all of Dune's features are supported in pesy (PRs
welcome). pesy
doesn't intend to duplicate Dune's efforts - it's
meant to compliment it. For simple use cases, pesy
wants to provide
a NPM friendly interface to Dune so that newcomers can quickly get
started, without confusing themselves with the library vs packages
nuances.
Every library, as we know, exposes a namespace/module under which it's APIs are available. However, as package authors, it can hard to make sure one is not using a namespace already taken by another package (Otherwise it could lead to collisions). Pesy works around this by assigning the library the upper camelcase of the root package name and directory the library/sub-package resides in.
Example: if a package.json looks like this
{
"name": "@myscope/foo",
"buildDirs": {
"library": { ... }
}
}
Then, subpackage library
takes a namespace of
MyScopeFooLibrary
. As a user, however, you shouldn't have to worry
much about yourself, since you can specify how you'd like to import
subpackages (and packages). In the above example, another subpackage
would consume it as follows
{
"name": "@myscope/foo",
"buildDirs": {
"library": { ... },
"anotherLibrary": {
"imports": [
"ThatOtherLibrary = require('@myscope/foo/library')"
]
}
}
}
And if you were consuming this package (after having published to npm), you can import it as follows:
{
"name": "bar",
"buildDirs": {
"library": {
"imports": [
"ThatFooLibrary = require('@myscope/foo/library')"
]
}
}
With the new NPM like conventions, pesy automatically handles the namespace for you so that we don't have to worry about the nuances of a package and a library during development.
pesy
generates dune files on fly behind the scenes. And to be able to so,
it needs a static dune file that looks like the following
(* -*- tuareg -*- *)
open Jbuild_plugin.V1
let () =
run_and_read_lines ("pesy dune-file " ^ Sys.getcwd ())
|> String.concat "\n"
|> send
These have to created only once - after that they never change (unless you decide
to eject). Every bootstrapped project has them already and you dont have to create
them. In case, you need to re-generate them, run esy pesy
(or esy @mysandbox pesy
if you are using esy
sandboxes). A common need for running esy pesy
is when you add a new folder to your project.
It is always possible to eject out of pesy config by running esy pesy eject ./subpackage-path
Compilation artifacts built on the CI can be downloaded by pesy warmup
. At
the moment, we only support Azure Pipelines can be configured as follows.
{
"pesy": {
"azure-project":
"<azure-project>/<azure-pipeline-name>",
"github":
"<github-org>/<github-repo>"
}
}
This will fetch appropriate artifacts compiled for the current
machine. Note however, the best way to get this feature to work is to
use cache publish and restore mechanism provided bootstrapped
files. pesy
assumes that the artifact zip file names and/or paths
haven't changed.
Compilation artifacts created by esy
are relocatable - since esy
sandboxes isolated, all the dependencies are accounted for and each
dependency is loaded from a path with a fixed-length prefix. Such
artifacts can be built on one machine and used on other provided the
prefixes are rewritten to reflect the updated path on the new
machine. And esy
provides all the low level command to do this out of
the box. pesy
simply provides a convenient wrapper that drives them.
When the project is bootstrapped for the first time, it is identical to a copy
that is run on the server. This is where the first set of cached builds come from,
which is why the azure-project
in the pesy config is set to esy-dev/esy
and
github
to esy/pesy-reason-template
.
Once the project sees changes, you most probably would add more dependencies
(or remove some) which could change the build sandbox state. It is recommended that
you run the provided CI setup to cache builds on your own Azure Pipelines instance
and update github
and azure-project
accordingly.
Easiest way to get started with distributing you library is to publish the source to NPM.
Let's take a look at an example.
Consider a base package foo
that you created and distributed on NPM. And let's assume, bar
is the package that consumes foo
.
$ pesy --directory foo-lib
This would have bootstrapped a project with the default template with
Util.re
in the library/
folder.
Let's publish it
$ npm publish
Let quickly create a new project, bar
and add foo
.
$ pesy --directory bar
$ esy add foo-lib
We can now require foo
"buildDirs": {
"library": {
"imports": [
+ "FooLib = require('foo-lib/library')"
]
}
}
And then edit Utils.re
let foo = () => {
FooLib.Util.foo();
print_endline("This is from bar");
};
$ esy
$ esy start
Hello from foo!
This is from bar
To use a custom template run pesy --template=github:your-name/your-pesy-template
This is a experimental feature that could see a lot of churn. We request you to watch the issue tracker for updates.
It works by downloading a git repo and then replacing special strings in filenames and files inside the repo. The special strings are currently these:
In filename | Replaced with |
---|---|
__PACKAGE_NAME__ |
package_name |
__PACKAGE_NAME_FULL__ |
package_name |
__PACKAGE_NAME_UPPER_CAMEL__ |
PackageName |
In contents | Replaced with |
---|---|
<PACKAGE_NAME> |
package_name |
<PACKAGE_NAME_FULL> |
package_name |
<PACKAGE_NAME_UPPER_CAMEL> |
PackageName |
<VERSION> |
version |
<PUBLIC_LIB_NAME> |
package_name/library |
<TEST_LIB_NAME> |
package_name/test |
Best way to get started creating a new template is to download
https://github.com/esy/pesy-reason-template and work on it. Any
changes can be tested with pesy test-template
.
This is reference guide explaining the config pesy
supports.
Configuration that applies to subpackages that create binary executables. Note that these executables can be ocaml bytecode or native binaries (ELF/Mach/PE)
A subpackage produces binary when it contains a bin
property.
{
"buildDirs": {
"src": {
"bin": "Main.re"
}
}
}
Here is a complete, working example
An array of advanced linking
modes. Each
string should be of the form "compilation-mode
binary-kind
" where
compilation-mode
is one byte, native or best and binary-kind
is
one of c, exe, object, shared_object.
{
"buildDirs": {
"src": {
"bin": "Foo.re",
"modes": [ "native", "exe"]
}
}
}
Here is a complete, working example
A string that maps to Dune's public_name
. Usually unnecessary (as
bin
property takes care of it) and must only be used to override.
A string that maps to Dune's name
. Usually unnecessary (as bin
property takes care of it) and must only be used to override the entry
point.
modes
can be used to configure the compilation target - native or
bytecode An
array of string, any of byte
, native
, best
This mode generates byte code output
This mode generates native output
Sometimes it may not be obvious if native compilation is supported on a
machine. In such circumstances, use "best"
and "native"
will be picked for
you if it's available. Else, it'll be "byte"
When writing C stubs to FFI into a library, simply mention the file
name (without the .c
extension) in the cNames
field.
{
"buildDirs": {
"src": {
"cNames": ["my-stub1", "my-stub-2"]
}
}
}
From dune version 2.0 onwards the cNames field was removed and foreignStubs field
was introduced to provide the FFI functionality, foreignStubs is a list of objects,
where each foreignStub object should specify language
, names
& flags
.
Incase names
& flags
is not specified or empty, their default value will be considered
Refer this for default values for names & flags
{
"buildDirs": {
"src": {
"foreignStubs": [
{
"language": "c",
"names": ["my-stub1", "my-stub-2"],
"flags": ["-verbose"]
}
]
}
}
}
Here is a complete, working example
imports
can be used to import a library (from a subpackage or an
external npm/opam package) and utilise the namespace of the imported
library.
{
"buildDirs": {
"src": {
"imports": [
"Console = require('console')"
]
}
}
}
The above config makes a namespace Console
available inside the
subpackage src
. Now any .re
file inside src
can use the
console
library.
// src/SomeFile.re
let foo = () => Console.log("Hello, world")
We can also import a package/subpackage under a different namespace
{
"buildDirs": {
"src": {
"imports": [
"NotConsole = require('console')"
]
}
}
}
And we can import (oddly confusing) NotConsole
from in src
// src/SomeFile.re
let foo = () => NotConsole.log("Hello, world");
We can also import other subpackages in the project.
{
"buildDirs": {
"src": {
"bin": "Main.re",
"imports": [
"FooConsole = require('console/lib')",
"MyOwnLibrary = require('../my-own-lib')"
]
},
"my-own-lib": {}
}
}
Here is a complete, working example
flags
- These flags will be passed to both the bytecode compiler and native compilerocamlcFlags
- These will be passed toocamlc
- the bytecode compilerocamloptFlags
- These will be passed toocamlopt
- the native compilerjsooFlags
- These will be passed tojsoo
compiler - the javascript compiler. Note: This is unrelated to Bucklescript
preprocess
accepts options needed to pass the source files via a
preprocessor first. When using custom syntax not natively supported in
the compiler, we pass the sources in a subpackage via a preprocessor
first.
For example, suppose we'd like to use let%lwt
syntax for our Lwt
promises
let%lwt foo = Lwt.return("foo");
print_endline(foo);
// instead of
Lwt.return >>=
(foo => print_endline(foo); Lwt.return())
We specify lwt_ppx
in pps preprocess
{
"buildDirs": {
"src": {
"bin": "Main.re",
"preprocess": ["pps", "lwt_ppx"]
}
}
}
Here is a complete, working example
Default is "no", and changing to "unqualified" will compile modules at deeper directories.
It's always possible that there are features Dune offers are needed and
the options above are not enough. Use rawBuildConfig
to add
options in a given library or binary. Use rawBuildConfigFooter
to
add config to the footer.
Example
{
"src": {
"rawBuildConfig": [ "(libraries unix)" ],
"bin": "Main.re"
}
}
Example
{
"src": {
"rawBuildConfigFooter": [
"(install (section share_root) (files (plaintext.txt as asset.txt)))"
]
}
}
Here is a complete, working example
Clone the repo and run esy
on it.
./_build/install/default/bin
would contain (after running esy
) Runner.exe
. Runner.exe
looks for PESY_CLONE_PATH
variable in the environment to find pesy
source. Set it to the path where the project was cloned.
To test if simple workflows work as expected. They assume both esy
and pesy
are installed
globally (as on user's machines).
version 0.4.0 (12/21/2018)
- Allow
buildDirs
to contain deeper directories such as"path/to/my-lib": {...}"
. - Added support for
wrapped
property on libraries. - Added support for
virtualModules
andimplements
- properties for Dune virtual libraries. (This will only be supported if you mark your project as Dune 1.7 - not yet released). - Stopped using
ignore_subdirs
in new projects, instead using(dirs (:standard \ _esy))
which only works in Dune1.6.0+
, so made new projects have a lower bound of Dune1.6.0
. - Support new properties
rawBuildConfig
which will be inserted at the bottom of the target being configured (library/executable).- It expects an array of strings, each string being a separate line in the generated config.
- Support new properties
rawBuildConfigFooter
which will be inserted at the bottom of the entire Dune file for the target being configured.- It expects an array of strings, each string being a separate line in the generated config.
- Support new properties
modes
for binaries and librarieslist(string)
.