In the previous post we built the project using esy/pesy
only. Check it out here.
To recap, I am trying to use the aantron/luv library to try to build something and I ran into all kinds of build system issues mostly because I still haven't grokked the reason-native build system yet. That's ok. This note is my way of getting better and having a reference I understand when I need to come back to it and giving back to the generous community. The reason-native
build environment has a list of great tools you can use. Most prominent among them are Dune
, Esy/Pesy
and Spin
. Here we are using Spin
.
Getting Started/Scaffolding with Spin
You can install spin
every which way you can think of. homebrew
,bash
,opam
, npm
, source
,curl
whatever. Go to the readme to pick one.
I used brew install tmattio/tap/spin
.
Running spin new native luv-cli
creates a new directory from where ever you are in the file system then give you a bunch of prompts. This really feels like professional grade tooling.
I answered 1
to all the questions to keep this ReasonML
all the way through.
❯ pwd
~/Github
❯ spin new native luv-cli
📡 Updating official templates.
Done!
Project name: luv-cli
Project slug: [luv-cli]
Description: [A short, but powerful statement about your project] luv wrapped in a cli
Which syntax do you use?
1 - Reason
2 - OCaml
Choose from (1, 2): 1
Which package manager do you use?
1 - Esy
2 - Opam
Choose from (1, 2): [1] 1
Which test framework do you prefer?
1 - Rely
2 - Alcotest
Choose from (1, 2): [1] 1
Which CI/CD platform do you use
1 - Github
2 - None
Choose from (1, 2): [1] 1
> cd luv-cli
This will produce the following directory structure:
> tree -L 2
.
├── CHANGES.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── _esy
│ └── default
├── bin
│ ├── dune
│ └── luv_cli_app.re
├── dune-project
├── esy.json
├── esy.lock
│ ├── index.json
│ ├── opam
│ └── overrides
├── lib
│ ├── dune
│ ├── utils.re
│ └── utils.rei
├── luv-cli.opam
├── luv-cli.opam.template
├── node_modules
└── test
├── _snapshots
├── dune
├── support
└── utils_test.re
11 directories, 16 files
Test It
Run esy start
or esy x luv-cli.exe
❯ esy start
Hello World!
Adding Color
If you haven't noticed yet, I'm basically following Micheal Kohl's post native-cli-apps-in-reasonml-part-1 to figure out how to add dependencies to a spin project. In the pesy-with-luv
build adding the luv
dependency worked out well. But I can't figure out how to add it to a spin project with getting build errors. Since Kohl, is a contributor, I am figuring he will show us, so let's see.
Let add the dependencies we will need:
esy add @reason-native/console
esy add @reason-native/pastel
Next we have to add them to the Dune configuration file in lib/dune
. In this file we’ll change the libraries
stanza from
`(libraries base)`
to
`(libraries base console.lib pastel.lib)`
Note how this is diffent than when we used pesy
. pesy
asks us to add our new deps to package.json
and rebuilds our dune
file for us.
We can use our new tools in a new Utils.hello
function. Update utils.re
with:
open Pastel;
let hello = () =>
<Pastel bold=true color=Green>
"Hello, "
<Pastel italic=true color=Red> "World!" </Pastel>
" 👋"
</Pastel>;
We also have to had this function to utils.rei
as you will have noticed if you tried to run esy start
.
Another pesy/spin
difference is that you can run your the project as soon as you make the changes. With pesy
we had to run esy pesy
then esy
then our run command.
With spin
you just run esy start
. I noticed that it did not work the first time you run esy start
but runs the second time without making any changes. Feels like the build system is lagging maybe? I have no idea.
Running esy start
will now get you:
~/Github/luv-cli
❯ esy start
Hello World!
Hello, World! 👋
Adding a Test
spin
generated a test file for us when we ran the spin cli.
open Test_framework;
open Luv_cli;
/** Test suite for the Utils module. */
let test_hello_with_name = (name, {expect}) => {
let greeting = Utils.greet(name);
let expected = "Hello " ++ name ++ "!";
expect.string(greeting).toEqual(expected);
};
describe("Utils", ({test, _}) => {
test("can greet Tom", test_hello_with_name("Tom"));
test("can greet John", test_hello_with_name("John"));
});
Let's add another one for our new function:
open Luv_cli;
/** Test suite for the Utils module. */
describe("Utils", ({test, _}) =>
test("Utils.hello() returns a greeting", ({expect}) => {
expect.string(Utils.hello()).toMatch("👋")
})
);
Running esy test
gets us 3 passing tests including our Utils.hello
test.
Again, all we had to do was run esy test
. We didn't have to rebuild. I'm finding that super convenient.
Here is what we got:
~/Github/luv-cli
❯ esy test
Running 2 test suites
PASS Utils
PASS Utils
Adding Argument Parsing for Practice
Run esy add @opam/cmdliner
the add cmdliner
to lib/dune
like we did before.
Our libraries
stanza now looks like this:
(libraries base console.lib pastel.lib cmdliner)
stanza
, by the way, as used in poetry, is what dune calls each line defining some behavior in project build. wikipedia. Appropriate use of the word, I think.
So now that we have Commandliner
let's use it in bin/luv_cli_app
.
Add the following to bin/luv_cli_app
:
open Cmdliner;
let cmd = {
let doc = "Simple CLI for Luvers built in Reason";
let who = {
let doc = "Who do you want to greet";
Arg.(
required
& pos(0, some(string), None)
& info([], ~docv="WHO", ~doc)
);
};
let run = who => {
Console.log @@ Utils.helloInput(who);
};
Term.(const(run) $ who, info("luv-cli", ~doc));
};
let () = Term.exit @@ Term.eval(cmd);
Notice I have cmd
calling Utils.helloInput
. I wanted to keep the other functions as they are to compare so lets add another to utils.re
and utils.rei
.
Here we change the previous Utils.hello
so that it takes a string, and rename it Utils.helloInput
let helloInput = (string) =>
<Pastel bold=true color=Green>
"Hello, "
<Pastel italic=true color=Red> string </Pastel>
" 👋"
</Pastel>;
Then add let helloInput: string => string;
to utils.rei
.
Run esy start Luvers
where Luvers
is just the string the command expects.
❯ esy start Luvers
Hello World!
Hello, World! 👋
Hello, Luvers 👋
What happened here? We used Open Cmdliner
in bin/luv_cli_app
but we didnt add it to bin/dune
. We only added it to lib/dune
and we did not even use it there. So I as a matter of structuring our project, we are including everything we need in our lib
and calling any library functions we might want to use from our lib
. Just for fun, take cmdliner
out of lib/dune
, add it to bin/dune
then run esy start
. You should get the same result. The way Kohl did it make sense though. It keeps control of everything in one place. After all, at this point, the only time we use cmdliner
is when calling it with from lib/utils.re
. This makes sense to me.
Adding Luv, First Attempt
esy add luv
, adding to lib/dune
:
(libraries base console.lib pastel.lib cmdliner luv)
then esy start
gets:
❯ esy start Luvers
File "lib/dune", line 5, characters 48-51:
5 | (libraries base console.lib pastel.lib cmdliner luv)
^^^
Error: Library "luv" not found.
Hint: try:
dune external-lib-deps --missing --root . --only-package luv-cli @@default
error: command failed: 'refmterr' 'dune' 'build' '--root' '.' '--only-package' 'luv-cli' (exited with 1)
esy-build-package: exiting with errors above...
error: build failed with exit code: 1
This got the best of me. I had to file an issue with the spin
repository. I will report back when I have figured this out.
Update 1
@tmattio got back to us here. Thanks Thibault!
This is what he had to say.
I believe the issue is that Result.get_ok comes from the standard library, but Spin-generated projects use Base. We took the decision to use the standard library instead of Base recently (see tmattio/spin-templates#10), but I didn't release a version with this change yet.
For now, you can either remove Base (by removing it from dune-project and every dune file), or you can use the equivalent of Result.get_ok in Base, which should be Result.return.
Seems like a simple answer, but really, there is a lot to unpack.
First thing I will do is try his suggestion to use the included Base.Result
type and see what happens.
Doesn't work, not without doing some other refactoring, anyway.
Committed to learning in public to share the knowledge. Luv
is using the StdLib.return
type so I am not going to chase this down right now but it would be a good learning exercise to switch out the types. So what is Base
. It's another library of standard utility functions created and battle tested at JaneStreet, the largest user of OCaml in production out there. Reading through the blog is recommended! I have this ocaml
search bookmarked.
The spin project has included Base
library and made it available globally. If we go to our project repo and open any dune
file we will see something like this:
(library
(name Luv_cli)
(public_name luv-cli.lib)
(modules (:standard))
(libraries base console.lib pastel.lib cmdliner)
(flags -open Base))
(include_subdirs unqualified)
See the (flags -open Base)
stanza? That is syntax for making Base
available globally in /library
or anywhere else you pass the flag. If we remove it, we should be on our way. I searched the project and removed and base
or -open Base
references, deleted the _esy
and esy.lock
directories and get some dune:the movie dialog references! Cool but not a solution.
Stand by for further updates.
Update 2 and Solution
The library maintainer, TMattio, chased this thing down. Apparently, the standard workflow was not working because a possible bug in esy
. You can see the discussion, here: [https://github.com/tmattio/spin/issues/73#issuecomment-617435857]. He aslo provided an alternative workflow to use in the meantime which is to build with esy
and dune
like we did in the Pesy-with-luv version. Remember that all these builds are using dune
and we still have access to dune
whichever we are using. If we build the spin project, luv-cli with esy dune build
the run with esy start
we get:
❯ esy dune build
~/Github/luv-cli adding-luv*
❯ esy start
Hello World!
Hello, World! 👋
luv-cli: required argument WHO is missing
Usage: luv-cli [OPTION]... WHO
Try `luv-cli --help' for more information.
~/Github/luv-cli adding-luv*
❯
Conclusions
- TMattio is an engaged library maintainer.
- We helped find a bug in the project by being engaged.
- Spin is a very nice alternative and will likely only get better.
Thanks for learning with me. Be sure to checkout Dune
, Esy/Pesy
and Spin
for your next projects.
Peace and love to you all.
Committed to learning in public to share the knowledge.
Top comments (0)