Stuff Needs to Build
gnumake

... this is a work in progress ...

Building code should be as easy as drinking coffee. A software developer is constantly building and rebuilding code throughout the development and test process. The build must be easy and efficient. It has to be so easy that it requires not thought since the developer is typically building while actually concentrating on code design and/or code debug. A complex build operation would destroy this concentration, or worse execute incorrectly and add frustration. Start a build, drink some more coffee: no thought.

Creating and maintaining the build must be as easy as learning how to work the corporate coffee machine. Every developer needs to be able to add source files and create new libraries with minimal, often no, training. This new work must also be communicated. The developer is extending the build. Either the developer has to personally communicate the extension to all other build users, or better, the extension should automatically transfer the changes into other developers' builds. The second is preferable since it does not require developer-to-developer communications. It is much like having the next developer walk up to the coffee machine and simply find coffee in the pot.

Developers, like coffee drinkers, are all different. In the end, the code is built and the coffee is in the stomach. It is the path to that end that varies. I was once part of a development team where at least four of the seven members had completely unique habits of writing and building code. I wrote code using emacs and built within the Korn shell on a Windows NT platform. Another developer did everything within Microsoft's Visual Studio. A third developer wrote code using CodeWright and built with the NT command prompt. The fourth developer used his Windows 95 laptop as an intelligent terminal to a Solaris box. He edited in VI on the Solaris and compiled using the C shell. All four of us were coding to the same project. We shared the same code and build. A single, good build process easily handled all these developer environments. The build, like the coffee, had to work cleanly for all of us (especially late at night when we were tired).

The coffee and coffee machine metaphor sounds nice, even easy. Unfortunately, the secondary issues related to building code can become quite complex. The coffee metaphor only holds for one or a few developers building code in their personal development environments. There could be multiple, different delivery environments, e.g. Linux and Windows. The build might also need to survive customer use when APIs or open source are appropriate. The choice is whether to deal with the complexity of issues up front, or ignore them until the issues come crashing in later.

Cost of ignoring issues

A build, simply stated, transforms human readable programming into a machine executable form. I believe every build I have ever seen succeeds in this basic task. The poor builds fail to address other issues that do not arise until later in the project's or company's evolution. Some of the commonly ignored issues are:

The horror stories that resulted from these ignored issues are numerous, and costly:

None of the individuals involved with these horror stories were stupid. They were just so occupied elsewhere that clean and efficient resolution to the issues was not the solution chosen. Throwing more people at the problem and/or living with the inefficiencies was the chosen solution.

I compare these horror stories to having a coffee machine without a coffee pot. Each person would start the coffee machine and walk away when their cup was full. The fact that the machine continued to produce a pot's worth of coffee for the carpet's consumption was not important. Let the cleaning folks deal with the problem instead of ordering a new coffee pot.

Requirements and tools

This build process implementation is exceedingly simple even though the requirements placed upon it are exceedingly large. Once in operation, it really is as easy to use as a coffee mug. It is also as easy to maintain as making a pot of coffee. The requirements list that follows restates most of the issues from the introduction:

  1. build files are uniform across all modules so as to support build/test automation and to create clear developer to developer communication,
  2. same build files used in native environment of Windows (command.exe, cmd.exe, or ksh.exe) and Linux (bash),
  3. a build for one module automatically builds all dependent libraries (and recursively builds any dependent libraries of the dependent libraries),
  4. 3rd party libraries also built by the process,
  5. global build capability extensions possible through single point of change for all modules (e.g. debug builds, profile builds),
  6. build process automatically creates include file dependencies and uses them, and
  7. when execute twice in succession, the build does nothing the second time, returning to the user immediately.

Some secondary requirements that are also achieved:

  1. the build structure allows multiprocessor (SMP) builds,
  2. the build structure minimizes, if not eliminate, operating system dependent ifdef's, and
  3. typical subdirectory layouts are easily interfaced.

Three key tools are necessary to achieve the requirements: gnumake (Windows and Linux versions), a pair of make file templates, and a dependency generator for Windows implementations.

gnumake version 3.79 is readily available for many operating environments. Version 3.77 is widely distributed but has several nasty bugs. One of 3.77's bugs can render this make setup useless when faced with deeply nested subdirectory paths. Please upgrade to 3.79 or better.

A pair of make file templates feed information to gnumake for this build process. One template, mvmake.mk, must exist in each module directory and be customized for that module. There is only one copy of the second template, basemake.mk. It contains generic rules and macros that integrate all mvmake.mk files at build time. Complete copies of the templates are found in the appendix and online (mvmake.mk and basemake.mk).

The Windows dependency generator extracts the list of include files found by the Microsoft preprocessor for use by gnumake. gnumake needs this information to be able to accurately determine which sources need to build after any one file anywhere in the build subdirectories changes. This dependency information is also stored within Microsoft's .idb files. However, there is no published interface to the .idb files. This build process extracts the information by re-running the preprocessor and reformats the information for gnumake via the mvdeps.exe tool.

Build process

gnumake orchestrates the entire build process based upon the input files provided. Conceptually, gnumake executes the build in three phases. No, the gnumake program does not execute exactly in this phased order. It actually operates in a more recursive fashion that fully or partially executes all three phases as it reads each file. I present the process as three phases because it is much simpler to communicate the concept. The final results of gnumake and this discussion appear the same to the developer. The key is to understand these contrived phases in order to understand how the build process works.

The phases are:

  1. Load all input files (mvmake.mk, basemake.mk, .d files)
  2. Create internal file dependency trees with separate variable names by module
  3. Execute build commands for all out of date files

gnumake phase I

This phase starts in the directory of the desired module. The mvmake.mk in the current directory defines the other modules it uses. It also specifies that gnumake read basemake.mk (which is always in a directory named mevlib). basemake.mk instructs gnumake to load the mkmake.mk for each of the modules specified by the initial mkmake.mk and any subsequent modules specified by these mkmake.mk's. So at minimum gnumake has to process two files, the initial mvmake.mk and basemake.mk. It could end up processing dozens of mvmake.mk files if that many modules are defined in the mvmake.mk files.

In Example 1, ./gadget is the current directory. The command "gnumake -f mvmake.mk" starts gnumake with gadget/mvmake.mk as the first input file. This initial mvmake.mk sets a variable ($M/MODULES_NEEDED) with the name of the modules it needs for compiling/linking. In this example, gadget/mvmake.mk needs the libthingies module so it contains the line:

    $M/MODULES_NEEDED := libthingies
gadget/mvmake.mk also informs gnumake to load basemake.mk from the standard mevlib directory. It is basemake.mk that processes $M/MODULES_NEEDED and informs gnumake to load libthingies/mvmake.mk also. This example uses three make files: gadget/mvmake.mk, libthingies/mvmake.mk, and basemake.mk.

Developers maintain the mvmake.mk files. The build process design minimizes the amount of information contained in these files so the developer can focus on the project, not the build. Developers must take each of the modules main deliverables (executables, libraries, etc.) and map those to their major source files. Developers do not need to map all of the secondary source files (include files) that the major source files use. The mvmake.mk and basemake.mk files contain information that allows gnumake to create and maintain a series of dependency files that end with ".d". These dependency files list all the secondary source files and their relation to each major source file. basemake.mk also instructs gnumake to also load the dependency files during this first phase of build processing. So basemake.mk and gnumake are automatically maintaining the secondary to major source file dependency lists. The developer does not have to worry about all the secondary source files that might be changing in other modules.

Example 1a shows the dependency files that the build process adds within the module directories. There is one .d file for each of the major source files. basemake.mk contains the instructions gnumake uses to create and maintain these files. gnumake builds the .d dependency files during Phase I processing.

The dependency files establish a relationship between major source files and every source file that is subsequently included during compiler preprocessing. The Unix compiler gcc has an option that causes its preprocessor to output a list of these included source files. basemake.mk uses this compiler option to create dependency files on a Linux platform. Microsoft's compiler has a similar feature, but the information is stored in a proprietary format. basemake.mk has to use alternate compiler options and the mvdeps.exe tool to create dependency files on a Windows platform.


gnumake phase II

The second phase of gnumake's processing builds a series of data trees. There is a tree for file name relationships and a tree for variable namespaces. gnumake populates these data trees based upon three major grammatical constructs in the input files (mvmake.mk, basemake.mk, and .d files).

gnumake phase III

The first action in Phase III is for gnumake to identify the file name or names in the file data tree that must be tested and potentially built. Either the developer specifies the file or files on the command line, or gnumake selects the target of the first rule it parsed in the input files. Developers typically do not specify the final target directly. So every mvmake.mk contains a default rule, "all : build" in the first few lines of the file. The target "all" becomes the file name that gnumake tests.

"all" and "build" are phony file names. They exist in the rules, but have commands listed in any input as to how to build them. gnumake assumes these phony files are older than any real file. This assumption ensures that the analysis of the file tree continues through the file data tree to real files. Any and all of the real files list as prerequisites to "build" in basemake.mk (and their prerequisites) will be built if old or missing. The phony file names are a simple method of reserving the default targets' definition to a later time in the parsing.

Example 1d shows how "all" and "build" fit into the example file data tree from Example 1c. They fit before gadget.exe. gnumake will attempt to see if "all" was up to date. In comparing it to "build", it discovers that it must test if gadget.exe is up to date before it can evaluate "build". The process continues recursively as it must now test the prerequisites of gadget.exe and each prerequisite's prerequisite, etc. Once the recursion completes, along with any builds necessary to get targets up to date with prerequisites, gadget.exe is up to date. At this point, gnumake marks the "all" and "build" file names as up to date by default since they have no commands as part of their rule.

[need sidebar about recursive versus inclusive makes ... reference paper]

[need to show how to make generic build rules in basemake.mk]

[map design back to requirements]

[maybe put in paragraph as to why folks should read build phases stuff: helps maintainers]

[maybe describe sections in terms of roles: user, maintainer, implementer]

Establishing standard directory layout

The build process, and in particular the make file templates, require a very simple subdirectory tree.

gnu make 3.79 will not work properly if you chose to nest several layers of builds within one module directory (several sub modules, discussed later). This can be corrected by applying a patch to rule.c of gnumake and recompiling. (Yes, this change was submitted and rejected by gnu. Others have reviewed it and also believe it to be appropriate.)