DEV Community

Alain
Alain

Posted on • Edited on

Spin With Luv, the Spin Build

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.

Alt Text

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 ocamlsearch 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

  1. TMattio is an engaged library maintainer.
  2. We helped find a bug in the project by being engaged.
  3. 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)