OCaml Programming with Emacs

Last update: 2013-07-01 00:42:53+0200

Required Software

opam install ocamlfind

opam install ocp-indent

Configuration

Add the following lines to your ~/.emacs or ~/.emacs.d/init.el file.

;; associate some file extensions to tuareg-mode
(add-to-list 'auto-mode-alist
  `(,(concat "\\." (regexp-opt '("ml" "mli" "mly" "mll")) "\\'") . tuareg-mode))

;; set up autoloads
(autoload 'tuareg-mode "tuareg" "Major mode for editing OCaml code" t)
(autoload 'camldebug "camldebug" "Run the Caml debugger" t)

;; make OCaml-generated files invisible to filename completion
(mapc #'(lambda (ext) (add-to-list 'completion-ignored-extensions ext))
  '(".cmo" ".cmx" ".cma" ".cmxa" ".cmi" ".cmxs" ".cmt" ".annot"))

;; Use ocp-indent to indent instead of Tuareg's default
(eval-after-load "tuareg"
  (let ((opamdir (car (split-string (shell-command-to-string "opam config var prefix")))))
    (load-file (concat opamdir "/share/typerex/ocp-indent/ocp-indent.el"))))

The ocp-indent tool comes with a number of configuration options to fine-tune your indentation preferences. These options can be set by editing (or creating) the file ~/.ocp/ocp-indent.conf. They can also be set on a per-directory basis by adding a file named .ocp-indent.conf in the directory. See the output of ocp-indent --help for more details.

Outline of the Method

  1. Use ocamlbuild as your build system. It is part of the OCaml distribution and generally involves the least amount of effort to get working.
  2. Create a Makefile in the project root that invokes ocamlbuild.
  3. Write a .ocamlinit file to set up the load directories and load your .cma files in the interactive toplevel.
  4. Add a check to Sys.interactive in the entry points of your delivered programs to ensure that they are not running in the interactive toplevel.

Each of the steps is illustrated below with a running example of a project called rot that delivers:

The library interprets a string of bytes as a big-endian number, rotates the bits of that number by a specified amount in a specified direction, and produces a corresponding different string of bytes of the same length. The program uses this library to obfuscate files. (This project is intentionally selected to be absolutely useless.)

Step 1. Laying out your project and using ocamlbuild

Begin by creating the following directory layout.

rot
rot/README             # optional
rot/_tags              # for ocamlbuild, explained below
src/Makefile           # for step 2
src/myocamlbuild.ml    # for ocamlbuild, explained below
rot/src                # all sources
rot/src/_tags          # for ocamlbuild, explained below

The three files relevant to this step are: rot/myocamlbuild.ml, rot/_tags, and rot/src/_tags.

rot/myocamlbuild.ml

This file, called a plugin, is the “build description” for the whole project.

We are going to use ocamlfind to build our sources. There is a ocamlbuild command-line flag -use-ocamlfind to instruct it to use ocamlfind, but to prevent any kind of accidents it is better to add it to the plugin.

open Ocamlbuild_plugin ;;

(* set the version number of the software in the plugin *)
let project = "rot"
let major = 0
let minor = 1
let patch = 0                           (* patch level *)
let tag   = ""                          (* build tag *)

(* generate src/version.ml in the plugin *)
let version_ml = "src/version.ml"
let version_file_contents =
  let buf = Buffer.create 255 in
  Printf.bprintf buf "(* This file is automatically generated!\n" ;
  Printf.bprintf buf "   To change version numbers, edit myocamlbuild.ml *)\n" ;
  Printf.bprintf buf "let major = %d;;\n" major ;
  Printf.bprintf buf "let minor = %d;;\n" minor ;
  Printf.bprintf buf "let patch = %d;;\n" patch ;
  Printf.bprintf buf "let tag   = \"%s\";;\n" tag ;
  Printf.bprintf buf "let str   = \"%d.%d.%d%s%s\";;\n"
    major minor patch
    (if tag = "" then "" else "-") tag ;
  Buffer.contents buf
let make_version msg =
  Printf.printf "(Re)Generating %S...%!" version_ml ;
  let ch = open_out version_ml in
  output_string ch version_file_contents ;
  close_out ch ;
  Printf.printf " done.\n%!"

let () =
  Options.use_ocamlfind := true ;
  make_version ()
;;

(* the entry point of the plugin *)
let _ =
  dispatch begin function
    | After_rules ->
      flag ["ocaml" ; "compile"] (A "-annot") ;
      flag ["ocaml" ; "compile"] (A "-g") ;
    | _ -> ()
  end

rot/_tags

The various tags files contain options for ocamlbuild. The purpose of the _tags file in the project root is to just point at the soruce directories.

<src> : include

rot/src/_tags

The real tags files live in the source directories. It is here that we indicate project dependencies. Suppose rot depends on the batteries library. We would write this as:

true : package(batteries)

At this point, you can run ocamlbuild version.cmo from the rot directory and have it generate and compile src/version.ml.

Step 2: The Makefiles

Create a Makefile in the project root (rot) and in every source directory.

rot/Makefile

OCBFLAGS := -classic-display
OCB := ocamlbuild $(OCBFLAGS)

.PHONY: all debug clean top
all: rotlib.cma rotlib.cmxa rot.native
debug: all rot.cma

%.cma:
	$(OCB) $@
%.cmxa:
	$(OCB) $@
%.native:
	$(OCB) $@

clean:
	$(OCB) -clean
	$(RM) src/version.ml*

top: debug
	ocaml

rot/src/Makefile

.PHONY: default
default: all

%:
	cd .. && ${MAKE} $@

Now you can run make from the project root and from every source directory.

Some points to note.

To make sure that everything works, you need two additional files.

rot/src/rotlib.mllib

Version

rot/src/rot.ml

(* empty for now *)

Now you can run make from any directory to create both the library and the program.

Step 3: Load everything using .ocamlinit

The interactive toplevel is used to debug both the library and the program. Its purpose is to load the required libraries, set up the load paths, and then load the various .cmas.

rot/.ocamlinit

(* -*- mode: tuareg; -*- *)
#use "topfind" ;;
(* require all dependencies *)
#require "batteries" ;;
#directory "_build/src" ;;
(* load all local packages *)
#load "rot.cma" ;;

Now running make top form any directory will go to the project root, rebuild anything needed, and call the interactive toplevel. This is handy when editing an OCaml file: if you type C-c C-s to start the toplevel from inside Emacs, it will ask for the command to run. Enter “make top” for this command.

This is particularly useful when debugging a file. If you kill any running toplevels (C-c C-k) and then type C-c C-b to load the current buffer, it will first load everything else and then load the buffer. If the .ocamlinit is set up properly, then all module references in the file, including references to modules in the same program, will be valid.

Step 4: Making the programs loadable

The entry points of the programs must not fire when run from inside the toplevel. Fortunately, this is easy to achieve – just check !Sys.interactive.

For example, here is how the rot program’s entry looks.

rot/src/rot.ml

let main () = ...

let () = if not !Sys.interactive then main ()

Now, when rot.cma is loaded in the toplevel (with make top), the main procedure of rot.ml is not executed.