Monday, February 18, 2013

non-recursive automake

You probably know Recursive Make Considered Harmful article. The simple way how to implement non-recursive build-system is to use one top level Makefile. Well, autotools are the best, so we will write Makefile.am rather than directly Makefile :-)

For more details about the basic non-recursive make see Flameeyes's autotools-mythbuster. I'd like to talk about something more advanced.

The problem is that maintain all in one huge Makefile.am is pretty painful, especially if your project uses many subdirectories. From my point of view it's better to maintain make rules at the same place (same directory) like the code.

Fortunately automake is smart enough to generate one huge Makefile from many .am files. The solution is "include" automake directive.

For example (top level Makefile.am):
  include foo/Makemodule.am
  include bar/Makemodule.am
where foo/ and bar/ are sub-directories and Makemodule.am is sub-directory specific automake stuff. You can use another name if you don't like Makemodule.

The important is to understand that in this case the result will be one Makefile. It means that things like usrbin_PROGRAMS are interpreted as global variables. The ideal solution is to define all the global variables before you include Makemodule.am and use += operator in the Makemodule.am files. For example:
top level Makefile.am:
   usrbin_PROGRAMS =
   man_MANS =
 
   include foo/Makemodule.am
   include bar/Makemodule.am
foo/Makemodule.am:
   usrbin_PROGRAMS += myprog
   man_MANS += foo/myprog.8
   myprog_SOURCES = foo/myprog.c \
                    foo/myprog-utils.c
Don't forget to use complete paths for all your project files (e.g. foo/myprog.c). The compiled binaries (final programs) will be stored in top-level directory, things like .o files will be store in the sub-directories.

Well, set some variables is pretty simple, but what about real make rules and automake hooks? Let say that program "abc" requires a special "make install" rule to create an "ABC" symlink:

top level Makefile.am:
    INSTALL_EXEC_HOOKS =

    include abc/Makemodule.am

    install-exec-hook: $(INSTALL_EXEC_HOOKS)
abc/Makemodule.am:
    install-exec-hook-abc::
         cd $(DESTDIR)$(usrsbin_dir); ln -sf ABC abc

    INSTALL_EXEC_HOOKS += install-exec-hook-abc
The trick is that you define INSTALL_EXEC_HOOKS global variable that points to all your sub-directory specific rules and the real automake "install-exec-hook" depends on this variable.

Note that you can use automake conditionals, for example:
  if LINUX
  ... 
  endif
for all the stuff.

The result is build-system with small readable subdir/Makemodule.am files and one top level maintainable Makefile.am.

The final top-level Makefile generated from your Makefile.am will be almost always faster. For example util-linux (make -j):
           recursive | non-recursive
           ----------+--------------
   2 cores: 14.5 sec | 13.2 sec
  16 cores:  9.5 sec |  4.3 sec
... but the numbers are not so important. After more than 6 months with non-recursive build-system I have to say that subdir/Makemodule.am based solution is better, because:
  • all variables are shared, all is initialized on one place in top-level Makefile.am
  • dependences between programs and libs (yeah, I use libtools) work as expected without extra make rules
  • because all is interpreted from top level directory I don't have to care about correct $(srcdir) and $(top_strcdir) within sub-directories
  • all final binaries are on one place, just type "make prog; ./prog" to test the program without care about a sub-directory. For recursive build-system you have to use "make -C subdir prog; ./subdir/prog".
That's all.