2 Platform-Independent Make Operational Details

2.2 The MakeRule command

The makerules.tcl files consist primarily of a collection of MakeRule commands surrounded by a sprinkling of Tcl support code. The order of the MakeRule commands doesn’t matter, except that the first target in the file, usually all, becomes the default target. (The “default” target is the effective target if pimake is run without specifying a target.)

The MakeRule command supports a number of subcommands, but the principle subcommand appearing in makerules.tcl files is Define. This takes a single argument, which is a list of option+value pairs, with valid options being -targets, -dependencies, and -script. The value string for the -targets option is a list of one or more build targets. The targets are usually files, in which case they must lie in the same directory or a directory below the makerules.tcl file. The -dependencies option is a list of one or more files or targets that the target depends upon. The value to the -script option is a Tcl script that is run if a target does not exist or if any of the file dependencies have a newer timestamp than any of the targets. The dependency checking is done recursively, that is, each dependency is checked to see if it up to date with its own dependencies, and so on. A target is out of date if it is older than any of its dependencies, or the dependencies of the dependencies, etc. If any of the dependencies is out of date with respect to its own dependencies, then its script will be run during the dependency resolution. The script associated with the original target is only run after its dependency resolution is fully completed.

The following examples from oommf/app/omfsh/makerules.tcl should help flesh out the above description:

  MakeRule Define {
    -targets        [Platform Name]
    -dependencies   {}
    -script         {MakeDirectory [Platform Name]}
  }

Here the target is the platform name, e.g., windows-x86_64, which is a directory under the current working directory oommf/app/omfsh/. There are no dependencies to check, so the rule script is run if and only if the directory windows-x86_64 does not exist. In that case the OOMMF MakeDirectory routine is called to create it. This is an important rule because the compilation and linking commands place their output into this directory, so it must exist before those commands are run.

Next we look at a more complex rule that is really the bread and butter of makerules.tcl, a rule for compiling a C++ file:

  MakeRule Define {
    -targets        [Platform Objects omfsh]
    -dependencies   [concat [list [Platform Name]] \
                            [[CSourceFile New _ omfsh.cc] Dependencies]]
    -script         {Platform Compile C++ -opt 1 \
                             -inc [[CSourceFile New _ omfsh.cc] DepPath] \
                             -out omfsh -src omfsh.cc
                    }
  }

In this example the target is the object file associated with the stem omfsh. On Windows this would be windows-x86_64/omfsh.obj. The dependencies are the platform directory (e.g., windows-x86_64/), the file omfsh.cc, and any (non-system) files included by omfsh.cc. Directory timestamps do not affect the out-of-date computation, but directories will be constructed by their MakeRule if they don’t exist.

Note that part of the -dependencies list is

  [CSourceFile New _ omfsh.cc] Dependencies]

As discussed earlier, this command resolves to a list of all non-system #include header files from omfsh.cc, or header files found recursively from those header files. The first part of omfsh.cc is

  /* FILE: omfsh.cc                 -*-Mode: c++-*-
   *
   *      A Tcl shell extended by the OOMMF core (Oc) extension
   ...
   */

  /* Header files for system libraries */
  #include <cstring>

  /* Header files for the OOMMF extensions */
  #include "oc.h"
  #include "nb.h"
  #include "if.h"

  /* End includes */
  ...

The header file cstring is ignored by the dependency search because it is specified inside angle brackets rather than double quotes. But the oc.h, nb.h, and if.h files are all considered. These files are part of the Oc, Nb, and If package libraries, respectively, living in subdirectories under oommf/pkg/. The file oommf/pkg/oc/oc.h, for example, will be checked for included files in the same way, and so on. The full dependency tree can be quite extensive. The pimake application supports a -d option to print out the dependency tree, e.g.,

  tclsh oommf.tcl pimake -cwd app/omfsh -d windows-x86_64/omfsh.obj

This output can be helpful is diagnosing dependency issues.

The /* End includes */ line terminates the #include file search inside this file. It is optional but recommended as it will speed-up dependency resolution.

If omfsh.obj is older than any of its dependent files, then the Tcl script specified by the -script option will be triggered. In this case the script runs Platform Compile C++, which is the C++ compiler as specified by the oommf/config/platforms/<platform>.tcl file. In this command -opt enables compiler optimizations, -inc supplements the include search path for the compiler, -out omfsh is the output object file with name adjusted appropriately for the platform, and -src omfsh.cc specifies the C++ file to be compiled.

The rules for building executables and libraries from collections of object modules are of a similar nature. See the various makerules.tcl files across the OOMMF directory tree for examples.

In a normal rule, the target is a file and if the script is run it will create or update the file. Thus, if pimake is run twice in succession on the same target, the second run will not trigger the script because the target will be up to date. In contrast, a pseudo-target does not exist as a file on the file system, and the associated script does not create the pseudo-target. Since the pseudo-target never exists as a file, repeated runs of pimake on the target will result in repeated runs of the pseudo-target script.

Common pseudo-targets include all, configure, help, and several clean variants. This last example illustrates the chaining of clean pseudo-targets to remove constructed files.

  MakeRule Define {
    -targets         clean
    -dependencies    mostlyclean
  }

  MakeRule Define {
    -targets         mostlyclean
    -dependencies    objclean
    -script          {eval DeleteFiles [Platform Executables omfsh] \
                         [Platform Executables filtersh] \
                         [Platform Specific appindex.tcl]}
  }

  MakeRule Define {
    -targets         objclean
    -dependencies    {}
    -script          {
                      DeleteFiles [Platform Objects omfsh]
                      eval DeleteFiles \
                             [Platform Intermediate {omfsh filtersh}]
                     }
  }

All three of these rules have targets that are non-existent files, so all three are pseudo-targets. The first rule, for target clean, has no script so the script execution is a no-op. However, the dependencies are still evaluated, which in this case means the rule for the target mostlyclean is checked. This rule has both a dependency and a script. The dependencies are evaluated first, so the objclean script is called to delete the omfsh object file and also any intermediate files created as side effects of building the omfsh and filtersh executables. Next the mostlyclean script is run, which deletes the omfsh and filtersh executables and also the platform-specific appindex.tcl file. Note that none of the scripts create their target, so the targets will all remain pseudo-targets.