dastapov: (Default)
Dmitry Astapov ([personal profile] dastapov) wrote2011-11-10 11:33 am
Entry tags:

Ocamldep, ocamlbuild и прочие семь кругов ада

Это вторая часть рассказа про компиляцию окамл, в которой, как в сказке - чем дальше, чем страшнее.

Итак, мы с вами выяснили, что без топологической сортировки по зависимостям проект на ocaml не собрать. А чтобы эту сортировку выполнить, надо сначала вычислить, что от чего зависит.

Казалось бы, раз компилятор в процессе компиляции обнаруживает все ссылки на другие модули, ему и карты в руки (см. "gcc -MM" или даже "ghc --make"). Но нет, нормальные герои всегда идут в обход, и для ocaml поиском зависимостей занимается отдельная утилита под названием ocamldep.

Эта утилита умеет прожевать пачку исходников (как интерфейсов (*.mli), так и собственно модулей (*.ml)) и выдать на-гора информацию о зависимостях в двух видах:
* Либо в виде правил в синтаксисе make
* Либо в виде перечня имен используемых модулей

Возьмем игрушечный пример:
foo.ml
======
let foo = 42;;

bar.ml
======
let bar = Foo.foo + 1;;

bar.mli
=======
val bar : int;;

main.ml
=======
let () =
  Print.printf "%d\n" Bar.bar;;


Получается, что основная программа (main) зависит от bar, который в свою очередь зависит от foo. Для bar явным образом описан интерфейс, для foo он неявно выводится из реализации.

Наша цель - настроить правила раздельной компиляции для нашей любимой системы сборки. Для общности предположим, что это не make, и нам необходимо всего лишь получить от ocamldep информацию о зависимостях, а потом переписать ее (автоматически) в нужном нам формате.

При этом мы уже прочитали раздел про компиляцию в документации про ocaml и знаем, что при компиляции интерфейсов модулей (mli) нам надо иметь скомпилированными интерфейсы всех зависимостей (.cmi), а при компиляции самих модулей нам нужно иметь как скомпилированные интерфейсы (.cmi), так и реализации (.cmx) - последние для cross-module inlining.

Поехали!
adept> ocamldep -native *.ml *.mli 
bar.cmx: foo.cmx bar.cmi
foo.cmx:
main.cmx: bar.cmx
bar.cmi:


Что мы видим? Во-первых, хотя bar зависит от foo, нам говорят, что bar.cmi не зависит ни от чего, что есть очевидная ложь, т.к. для его компиляции нужен foo.cmi.

Во-вторых, файлы перечислены без всякого порядка, т.е. для линковки нам по-прежнему надо как-то сделать топологическую сортировку вручную. Это вообще без комментариев. (Тут, кстати, можно попенять и на make - какого фига он не умеет подставить в команду зависимости в порядке сборки?)

В третьих, хотя реализация bar (bar.cmx) зависит от интерфейса foo (foo.cmi), в зависимостях это не отражено.

Другими словами, мы не можем взять вывод от ocamldep и переработать его в правила для нашей любимой системы сборки без дополнительного, достаточно сложного анализа.


А все дело в том, что ocamldep пытается быть слишком умным. Поскольку foo.mli отсутствует, при компиляции foo.ml будут сгенерированны сразу и foo.cmi, и foo.cmx (то есть, и скомпилированный интейфейс, и реализация). Ocamldep использует это тайное знание и вместо зависимости от foo.cmi прописывает везде зависимости от foo.cmx, делая неявные предположения о том, как устроена наша система сборки и правила в ней!

Более конкретно, ocamldep предполагает, что мы не можем описать в нашей системе правила с условием вида "если есть foo.ml И foo.mli, то собираем foo вот так, а если есть только foo.ml - то собираем вот эдак". Таким образом, если для сборки используется что-то менее тупое, чем make, необходимо исправлять эту узкоспециализированность ocamldep постпроцессингом.

Самое смешное заключается в том, что даже в случае с make использование ocamldep ведет к проблемам. И вот почему: предположим, что со временем наш модуль foo вырос, и мы создали для него описание интерейса (foo.mli). Без повторной генерации информации о зависимостях оно ... не будет компилироваться, т.к. нигде нету зависимостей от foo.cmi!

Чтобы все заработало, надо собирать информацию о зависимостях перед каждой сборкой. А если в проекте есть автогенертруемые исходники (ocamllex, ocamlyacc или любая другая генерация программ программамит) - надо запускать ocamldep несколько раз. Но вот беда - у make все зависимости должны быть известны статически, соответственно надо извращаться с phony targets, временными файлами и т.п. Теперь представим, что проект - большой, разложенный по куче директорий, и добавим сюда известные проблемы с recursive make, и становится понятно, что ocamldep + make совсем не равно "любовь". Слишком часто приходится делать "make clean && make dep && make", поскольку ocamldep и make запутались в зависимостях от генерируемых исходников, и либо забывают их перегенерировать, либо теряют зависимости.

Казалось бы, как раз на этот случай авторы ocamldep реализовали второй режим работы ocamldep: "ocamldep -modules":
adept> ocamldep -modules *.ml *.mli 
bar.ml: Foo
foo.ml:
main.ml: Bar Print
bar.mli:


Тут вывод уже более разумный - действительно, реализаця bar ссылается на Foo, а основная программа использует модули Bar и Print.
Однако, с этим форматом вывода тоже работать нелегко:
1)Модуль Print - это модуль из стандартной библиотеки, почему он упомянут в выводе?
2)В нашем конкретном случае можно отобразить имя модуля "Foo" на файловую систему тривиальным образом (Foo -> foo.cmi/foo.cmx). В общем же случае, если мы используем, например, модуль List из библиотеки Data ("open Data; let baz = List.length []"), мы получим в выводе ocamldep упоминание и Data, и List одновременно, и нам придется самостоятельно решать, что есть что.
3)Топологическую сортировку для линковки надо по-прежнему делать самостоятельно, и не совсем понятно, как это делать.

Да, и на закуску: ocamldep может спокойно съесть некомпилирующийся исходник и найти в нем зависимости, которых реально нет.

Чтобы как-то жить с этим ужасом, было придумано целых два средства, живущих на следующем уровне абстракции: ocamlfind и ocamlbuild.

Про них - в следующем посте. Продолжение следует ...

Post a comment in response:

If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting