A Quick Guide to Quick Changes in MirageOS

MirageOS is a collection of libraries and a system for assembling them into unikernels. What happens if you want to make changes to those libraries and test them with a new unikernel?

Say, for example, I have a static website (like this blog) that I build in MirageOS. I want to make some changes to the TCP implementation against which the blog is built. In order to do that, I need to do all the following:

  • figure out which module to change
  • figure out which package provides that module
  • get the source for that package and instruct the package manager to use it instead of the release
  • make changes
  • reinstall the package with your changes
  • rebuild the unikernel completely
  • see whether changes had the desired effect

Here’s a quick primer on how.

What am I changing?

If I want to change the TCP implementation (say, to try to fix this bug), I’m going to need to make an alteration to the code that’s provided to my unikernel fulfilling the TCP module type signature. I want to run my site as a unikernel on a Xen hypervisor, so I’ll build it with mirage configure --xen, which decides which packages are most appropriate for providing the right functionality in the Xen context. Since I’m building with --xen, the only option available for networking is the direct stack (vs the socket stack available when building with --unix).

What package provides that?

There are a few ways I can discover which package is providing the TCP implementation. I can look directly into the mirage front-end tool and find the relevant section for the TCP module, which currently looks like this:

let tcp_direct_conf () = object
  inherit base_configurable
  method ty =
    (ip: 'a ip typ) @-> time @-> clock @-> random @-> (tcp: 'a tcp typ)
  method name = "tcp"
  method module_name = "Tcp.Flow.Make"
  method packages = Key.pure [ "tcpip" ]
  method libraries = Key.pure [ "tcpip.tcp" ]
  method connect _ modname = function
    | [ip; _time; _clock; _random] -> Printf.sprintf "%s.connect %s" modname ip
    | _ -> failwith "The tcp connect should receive exactly four arguments."

From method packages = Key.pure ["tcpip"], we can infer that the package providing this is tcpip. Now that we know what package we need to look up, we can get some more information, including where to clone it from and where to report issues. Here’s an abbreviated look:

$ opam info tcpip
             package: tcpip
             version: 2.6.1
          repository: default
        upstream-url: https://github.com/mirage/mirage-tcpip/archive/v2.6.1.tar.gz
       upstream-kind: http
   upstream-checksum: 128a4250716424d32c6e84f35502925a
            homepage: https://github.com/mirage/mirage-tcpip
         bug-reports: https://github.com/mirage/mirage-tcpip/issues
            dev-repo: https://github.com/mirage/mirage-tcpip.git

So now we know how to clone this package:

$ git clone https://github.com/mirage/mirage-tcpip

and we can make a new branch and commit some changes to it.

Pin that package in opam

Once we have a path or a git branch where we’ll make changes, we can instruct opam to refer to that version of the package rather than the released one we were working with before. I generally use path-pinning for ease of hacking without making a lot of intermediate git commits, but others might prefer to point opam at a branch.

$ opam pin add tcpip ~/mirage-tcpip

The initial pin will cause opam to reinstall the tcpip package. Now if we rebuild our unikernel, we’ll use the pinned version:

$ cd my_rad_unikernel
$ make clean
$ mirage configure --xen
$ make

We can verify that the unikernel has the behavior we desire to change after the pin (just in case someone’s already fixed the problem in the primary branch of the repository, but hasn’t yet made a release).

Make your changes

We’ll elide the details here, but it probably looks something like this:

$ cd mirage-tcpip
$ git checkout -b tcp_opt_parse
$ vi tcp/options.ml
$ git commit -m "got all the problems away" tcp/options.ml 

Reinstall the package via opam

Since we made a change, we need to reinstall the package so that when we rebuild the unikernel, the changes will be reflected in the referenced installed package. We can do this with opam reinstall:

$ opam reinstall tcpip

If we’ve broken the build with our changes, the package may fail to reinstall. That’s OK; we can fix it and just try opam reinstall tcpip again, and confirm that we want to install this package.

Rebuild the unikernel completely

We’ll redo the same steps we did when we initially pinned the package: clean the old state, reconfigure the unikernel, and rebuild it.

$ cd my_rad_unikernel
$ make clean
$ mirage configure --xen
$ make

See whether the changes worked

If we have a simple test script for our unikernel (or our unikernel is a test script, like this ARP-testing unikernel), we can automatically confirm whether our changes fixed the problem. (We may even be able to see whether our changes broke anything else!)

Commit upstream!

Finally, we might want to share our rad changes with others! MirageOS has documentation on making contributions and a list of ideas for contributions if you’d like some inspiration.