Coding by example, migrating ocsoap to OASIS and ocamlbuild
In my effort to automate most of the stuff needed to release, I am working on a SOAP to OCaml converter. Richard W.M. Jones has kindly allowed me to take-over its project "ocsoap". The project is now located here and I am working on making it compatible with FusionForge SOAP.
My first step was to make it compatible with OASIS and ocamlbuild. The project is Makefile based and needs some extra care to make it ocamlbuild based.
The oasis-fication itself was a piece of cake. Just describe the dependencies of the project and make some extra choice like compiling the camlp5 extension to a .cma rather than a .cmo. You can see the _oasis and myocamlbuild.ml results, compared to the initial Makefile.
The _oasis is simple:
OASISFormat: 0.3 Name: ocsoap Version: 0.7.1 Synopsis: SOAP converter to OCaml code Authors: Richard W.M. Jones, Sylvain Le Gall License: LGPL-2.1 with OCaml linking exception Plugins: DevFiles (0.3), META (0.3), StdFiles (0.3) BuildTools: ocamlbuild, cduce BuildDepends: dynlink, pxp-lex-utf8, pxp-engine, netclient, cduce, extlib, calendar, pcre Library ocsoap Path: src Modules: OCSoap Library pa_ocsoapclientstubs Path: src Modules: Pa_ocsoapclientstubs BuildTools: camlp5o BuildDepends: camlp5 FindlibParent: ocsoap FindlibName: syntax CompiledObject: byte Executable wsdl_validate Path: src MainIs: wsdl_validate.ml Executable wsdltointf Path: src MainIs: wsdltointf.ml Executable adwords_test1 Path: examples/adwords BuildDepends: ocsoap MainIs: test1.ml Build$: flag(tests) Install: false BuildTools: camlp5o BuildDepends: ocsoap.syntax Executable adwords_test2 Path: examples/adwords BuildDepends: ocsoap MainIs: test2.ml Build$: flag(tests) Install: false BuildTools: camlp5o BuildDepends: ocsoap.syntax Executable adwords_examples Path: examples/adwords BuildDepends: ocsoap MainIs: example.ml Build$: flag(tests) Install: false BuildTools: camlp5o BuildDepends: ocsoap.syntax Document "api-ocsoap" Title: API reference of OCSoap Type: ocamlbuild (0.3) BuildTools+: ocamldoc XOCamlbuildLibraries: ocsoap XOCamlbuildPath: src/
Most of it was generated using oasis quickstart, that helps you to write _oasis. This part was not tricky and mostly a translation of what the previous Makefile was explaining, in its language.
Now comes the tricky part, translating Makefile rules into ocamlbuild rules. This part is more tricky because the general principle behind Makefile and ocamlbuild are not exactly the same.
Let's start with a simple rule: compiling a CDuce .cd file into a .cdo.
Here is the Makefile rule:
%.cdo: %.cd $(CDUCE) --compile $<
In the oasis-fication, we have added an extra complexity, because we move the source to src, which needs extra flags.
Here is the ocamlbuild rule:
rule "cduce: %.cd -> %.cdo" ~prod:"%.cdo" ~dep:"%.cd" begin fun env build -> Cmd(S[cduce; T(tags_of_pathname (env "%.cd")++"cduce"++"compile"); A"--compile"; A"-I"; P(Filename.dirname (env "%.cd")); P (env "%.cd")]) end ;;
It is a little bit more complex, lets explain it.
rule create a rule with a name and a function that execute it. The labels
~dep are the right and left parts of
%.cdo: %.cd of the Makefile. When we call
env "%cd", it replaces the
% by the matching part of
Next we return a Rule.action. The rule in this case is a command
Cmd. This rules use a sequence
S, which are the command line itself. The sequence is made of smaller pieces that can be a placeholder for tag content (
T + what will trigger it), atom (
A, typically command line option), and filename (
For a file src/foo.ml, the command generated will
cduce $(TAG) --compile -I src src/foo.ml
$(TAG) will be replaced by the content of
flag ["file:src/foo.ml"; "cduce"; "compile"] content. But also by the content of
flag ["cduce"; "compile"] content, the flag just need to be subset. For example if you want to add
--verbose to the command line, just add
flag ["cduce"; "compile"] (S[A"--verbose"]);; somewhere in myocamlbuild.ml.
Next rules, we need to compile
%.cmo. It is trickier because in this case, we want to have
%.cmi eventually built before. It is not require to build it before, if there is no %.mli file.
Here is the code to do that:
let cduce_mkstubs includes = Quote (S([cduce; S(List.map (fun fn -> S[A"-I"; P fn]) includes); A"--mlstub"]));; (* cduce < 0.3.9: cdo2ml -static *) let cduce_compile_args env = [ A"-c"; A"-package"; A"cduce"; A"-pp"; cduce_mkstubs [Filename.dirname (env "%.cmi")]; A"-I"; P(Filename.dirname (env "%.cmi")); A"-impl"; P(env "%.cdo") ] ;; let cduce_prepare_compile env build = List.iter (function | Outcome.Bad _ -> (* Fail but it just means that the .cmi will be generated * during compilation. *) () | Outcome.Good _ -> ()) (build [[env "%.cmi"]]) ;; rule "cduce: %.cdo -> %.cmo" ~prod:"%.cmo" ~dep:"%.cdo" begin fun env build -> cduce_prepare_compile env build; Cmd(S( [ocamlfind; ocamlc; T(tags_of_pathname (env "%.cdo") ++"cduce"++"ocamlc"++"compile"++"byte")] @ (cduce_compile_args env))) end ;;
The difference with the former rules, is that we call
cduce_prepare_compile which in turns call
(build [[env "%.cmi"]]). The call to
build will ask ocamlbuild to compile src/foo.cmi, but we don't care about the result, i.e. in case of
Outcome.Bad exn we don't fail. This way we don't stop the build process that will continue and produce a .cmo and .cmi just out of the .cdo. The .cdo itself is compiled as a .ml file with
ocamlfind ocamlc except that we apply cduce --mlstub as a preprocessor
A"-pp"; Quote(S[cduce; ..; A"--mlstub"]).
The last rule that I will comment is the one that transform a .intf into a .ml. This rule is particular because it is totally different than the pieces of code of the original Makefile.
Here are the couple of rules that was needed to translate .intf:
examples/adwords/%Service.cmx: examples/adwords/%Service.intf ocamlfind ocamlopt $(OCAMLOPTFLAGS) -c \ -pp "camlp5o ./pa_ocsoapclientstubs.cmo -impl" -c -impl $< .depend: $(wildcard *.mli) $(wildcard *.ml) \ $(wildcard examples/adwords/*.mli) $(wildcard examples/adwords/*.ml) $(OCAMLDEP) $^ > .depend for f in examples/adwords/*.intf; do \ $(OCAMLDEP) \ -pp "camlp5o ./pa_ocsoapclientstubs.cmo pr_o.cmo -impl" $$f \ >> .depend; \ done
Here is the ocamlbuild rule:
rule "ocsoap: %.intf -> %.ml" ~prod:"%.ml" ~deps:(if !ocsoap_dev then ["%.intf"; !pa_ocsoapclientstubs] else ["%.intf"]) begin fun env build -> Cmd(S[Px !camlp5o; P !pa_ocsoapclientstubs; P "pr_o.cmo"; A"-impl"; P(env "%.intf"); A"-o"; P(env "%.ml")]) end ;;
Here we decided to translate directly the .intf into a .ml file. The good thing about ocamlbuild is that it has a powerfull dynamicy dependencies scheme. So here you will generate the .ml file, which in turns will generate a .ml.depends and then will be compiled the standard way. In the makefile, you need to compute the .depends file using a different process that will do everything before the compilation even starts (in fact, before the inclusion of the .depends). We also use the trick to use an OCaml printer (pr_o.cmo) with camlp5o, that will directly output the standard .ml file.