bmake: Meta Mode

This doc describes meta mode in bmake(1).

What it is

Make is a program for maintaining dependencies. The high level goal is to update targets whenever (but only when) they are out-of-date with respect to their sources. It does this by comparing file modification times.

In meta mode bmake has additional information to base its decisions on. When a target is being evaluated; if the normal rules do not indicate it is out-of-date, bmake will consult a .meta file which was recorded during the previous build of that target.

For example; this allows bmake to compare the script it would use to build the current target, with the one used last time. If they are different; the target is out-of-date. This is a much more powerful (and reliable) mechanism that simply comparing file modification times.

Further, if the filemon(4) driver is available, bmake is able to check the modification times of every file read during the previous build of the current target. This is far more information than can normally be expressed in makefiles (with any hope of maintainability) or captured by the traditional make depend.

Finally, if bmake is configured to know the scope of its control, any file that was created during the previous build, which is now missing can cause the current target to be out-of-date. This for example, allows knowing that an install or staging target needs to be re-run because the installed files have been removed.

Most of these features can be supressed on a per target (or makefile) basis, to cater for corner cases.

When bmake [re]creates a target, it generally creates a .meta file to capture all the data needed to enable the above. The .meta files are designed to be easy for humans to read, which is very useful when debugging build problems.

The .meta files are also easily parsed by shell scripts. This adds considerable value, by making it possible for makefiles to post process the .meta files, to do things like gathering directory dependencies. Thus meta mode becomes the foundation for writing smarter build systems (eg. the DIRDEPS_BUILD), rather than just better makefiles.

How it works

A .meta file simply collects information about the target that bmake is building. This includes the expanded commands run, the environment (optional and rarely useful - but handy when you need it), any command output, and finally a capture of interesting system calls performed by the commands. Such information can have many uses beyond simply ensuring targets are updated when they should be, not least of which is automating capture of tree wide dependencies.

The basic idea of .meta files originated in John Birrell's build project for FreeBSD. While the build prototype hard-coded all build behavior, and required all makefiles to be replaced, bmake provides the functionality, leaving policy to the makefiles (sys.mk etc). The vast majority of makefiles do not need to be touched in any way to take advantage of meta mode.

This doc aims to concentrate on the functionality added to bmake, but some makefiles are needed to make use of that functionality.

A number of such makefiles will be mentioned below. Some have a meta. prefix in their name - to distinguish them from other variants, and also to aid in their use along side the other variants since full migration can take a while.

Rationale

Why a new mode for make? To aid the automated capture of dependency information, and thus help optimize build performance.

Optimizing build performance means doing as little as possible, and on modern CPU's doing as much of it in parallel as possible. However, being quick is useless if the results are incorrect. Meta mode helps on all three fronts.

avoid make depend

The traditional BSD build model involved multiple tree walks. Collapsing the bmake depend and bmake all phases into one helps, but eliminating the bmake depend is even better.

For many years autodep.mk has leveraged the gcc -M* flags to gather dependency information as a side effect of compilation. This works fine for most makefiles, but not all when trying to build in parallel. Makefiles which omit important dependencies, require bmake depend or equivalent, before they can be successfully built in parallel.

By capturing local dependencies into Makefile.depend (see the example in Makefile.depend), we can successfully build a clean tree in parallel.

Also by leveraging filemon we can capture accurate dependencies for all targets, not just those built by gcc.

avoid unnecessary dependencies

There are also advantages to be had when re-building a tree. The fact that in meta mode, bmake can compare the actual expanded command lines when deciding if a target is out-of-date, means that one can avoid doing things like:

# if any of the makefiles have changed we need to regenerate
# this - "just in case"
generated.h:    ${.MAKE.MAKEFILES:N.depend}

which can often lead to lots of unnecessary re-compilation.

tree walks don't always cut it

Apart from being inefficient, using bsd.subdir.mk to walk a tree, can be a challenge if various leaf directories have dependencies on each other.

For many years, we have made use of tree-wide dependencies to have the build visit the leaf nodes of the tree directly - in the order required, with a high degree of parallelism.

The model described here allows the same functionality, but with less overhead. Just as autodep.mk collects the dependencies of a directory as a side effect of building, we can leverage the data collected in meta mode to derive tree dependencies as a side effect of building.

Of course one still needs to be able to launch a build in a non-leaf directory, so meta.subdir.mk supports that.

ALL_MACHINES

Another useful trick enabled by having Makefile.depend.${MACHINE} or another means of knowing which machines a directory should be built for, is that one can (from any location) do:

bmake -DALL_MACHINES

to tell dirdeps.mk to get a list of all the Makefile.depend.* in the current directory, and for each ${machine} add ${.CURDIR}.${machine} to the initial DIRDEPS. Thus one can easily check if a change, breaks any of the supported architectures.

If there are no Makefile.depend.*, but SUBDIR is set, then meta.subdir.mk will find Makefile.depend.* via SUBDIR.

sub-set checkouts

As noted below, in addition to capturing DIRDEPS (those directories which must be built before the current one), we can also capture SRC_DIRDEPS (those directories other than the current one, which must be present), which can be used to drive logic to checkout the minimal source needed to build a given directory.

Meta mode

Meta mode is enabled by the keyword meta appearing .MAKE.MODE.

Writing .meta files

Generally speaking, for each target to be made, a .meta file is created. This is normally named ${.TARGET}.meta. That is if cat is made from cat.o then, cat.o.meta and cat.meta will be used.

If the target is flagged .PHONY, .MAKE or .SPECIAL (eg. .BEGIN, .END, .ERROR), then a .meta file is not created unless the target is also flagged .META.

A .meta file is never created if the target is flagged .NOMETA.

If for some reason, a file is being generated outside of the current object dir the meta file will be named for the absolute path of the target with all / replaced with _.

If .OBJDIR is the same as .CURDIR, then .meta files will not be created unless curdirOk=yes appears in .MAKE.MODE.

In each .meta file bmake records:

  • the expanded command line, prefixed with CMD.
  • the environment, prefixed with ENV. (this is optional, see .MAKE.MODE).
  • the current directory prefixed with CWD.
  • the target, prefixed with TARGET.
  • the command output preceded by the line -- command output -- This can be extremely useful in diagnosing build breaks. See Error handling below.
  • syscall data collected from filemon preceded by the line -- filemon acquired metadata -- This too can be extremely useful in diagnosing bugs.

For each .meta file [re]created, bmake appends its name to the variable .MAKE.META.CREATED as well as .MAKE.META.FILES. These variables are not used directly by bmake but allow for simple auto dependency extraction.

Also, in meta verbose mode, whenever a .meta file is [re]created the variable .MAKE.META.PREFIX is expanded and printed (if not empty). The default value is Building ${.TARGET:H:tA}/${.TARGET:T}.

Reading .meta files

When evaluating targets, if bmake has not already decided to re-build the target, it consults the relevant .meta file, processing stops as soon as an out-of-date decision is made.

First it compares the CMD entries with the ones currently associated with the target. If the number of commands is different, the target is considered out-of-date. If any expanded command is different and .MAKE.MODE did not contain ignore-cmd and the target was not flagged .NOMETA_CMP, the target is considered out-of-date.

Next the sycall data is consulted. For each E and R entry, the pathname's modification time is checked and if newer than the target, it is considered out-of-date.

The name of each .meta file evaluated, is appended to the variable .MAKE.META.FILES.

Performance

Re-processing .meta files, does add overhead to the build. There are potentially a lot more stat(2) calls performed, especially when nothing needs to be done.

For example, building libc generates nearly 3000 objects. If on a given machine, in normal mode bmake takes about 5 seconds to decide there is nothing to do. In meta mode, it takes 6 seconds to reach the same conclusion. Nothing to do, is the worst case from the overhead point of view.

By contrast, when building a clean tree, generating the .meta files is equivalent to generateing the *.d files with gcc -M*. Considering all the extra information collected, this is promising.

The more common case of re-building after some changes have been made, is where the benefits are seen. The ability to ensure everything that needs to be rebuilt (and only those) throughout the tree, whether it improves build times or not, reduces the time spent working out why something didn't work as expected.

Error handling

When a target fails, bmake will set .ERROR_TARGET to its name and in meta mode, .ERROR_META_FILE to the name of the associated .meta file (if any).

These can be leveraged either by a .ERROR target, or simply included in the MAKE_PRINT_VAR_ON_ERROR list which bmake will print on error.

The fact that .ERROR_META_FILE names a file containing both the command output - presumably including any errors, as well as a record of all the files read, means that automated build break diagnosis can be greatly simplified.

For example:

# Meta data file /h/obj/NetBSD/5.X/usr.bin/make/make.o.meta
CMD cc -O2 -DMAKE_NATIVE -c /amd/mnt/swift/host/c/sjg/work/NetBSD/5.X/src/usr.bin/make/make.c
CWD /h/obj/NetBSD/5.X/usr.bin/make
TARGET make.o
-- command output --
/amd/mnt/swift/host/c/sjg/work/NetBSD/5.X/src/usr.bin/make/make.c:140:28: error:
 no-such-header.h: No such file or directory
*** Error code 1
-- filemon acquired metadata --
# filemon version 2
# Target pid 7006
V 3
R 3655 /etc/ld.so.conf
R 3655 /lib/libedit.so.2
R 3655 /lib/libtermcap.so.0
R 3655 /lib/libc.so.12
R 7342 /etc/ld.so.conf
R 7342 /usr/lib/libc.so.12
W 7342 /var/tmp//ccMaMdlI.s
R 4248 /etc/ld.so.conf
R 4248 /usr/lib/libc.so.12
R 4248 /amd/mnt/swift/host/c/sjg/work/NetBSD/5.X/src/usr.bin/make/make.c
W 4248 /var/tmp//ccMaMdlI.s
R 4248 /usr/include/sys/cdefs.h
R 4248 /usr/include/machine/cdefs.h
R 4248 /usr/include/sys/cdefs_elf.h
<skip-a-bit>
R 4248 /usr/include/sys/featuretest.h
X 4248 1
D 7342 /var/tmp//ccMaMdlI.s
X 7342 1
X 3655 1
# Bye bye

We can have something like:

meta_error_log = ${SB}/error/meta-${.MAKE.PID}.log

.ERROR:
        -@[ "${.ERROR_META_FILE}" ] && { \
        grep -q 'failure has been detected in another branch' ${.ERROR_META_FILE} && exit 0; \
        mkdir -p ${meta_error_log:H}; \
        cp ${.ERROR_META_FILE} ${meta_error_log}; \
        echo "ERROR: log ${meta_error_log}" >&2; }; :

to gather the meta-*.log files in a convenient location for automated analysis. Note that we suppress complaints about the build having failed elsewhere.

.MAKE.META_BAILIWICK

Naming stuff is hard ;-) This variable can be set to a list of directories or prefixes, that collectively describe bmake's scope of control. Normally this does not matter.

The exception is when the build is setup for staging (automatically installing files so they can be found by the rest of the build), if someone rm -rf part of that staging area, you really want the targets that populated it to be considered out-of-date.

When bmake is looking at the system calls done during the previous build, any absolute path written to, will be checked against his bailiwick. The restriction to absolute paths reduces the overhead and the risk of getting confused. The current directory and any temp dir are ignored. Any file which is now missing, is added to a missingFiles list. If a subsequent move or delete is seen the file is removed from the missingFiles list.

If the missingFiles is not empty after the syscall checks are complete, the target is out-of-date.

Debugging -dM

If meta mode debugging is enabled (-dM), bmake will report in detail why the meta file checks decided a target is out-of-date, or why a .meta file was not generated. This is useful for finding bugs:

Skipping meta for all: no commands
Skipping meta for .END: .SPECIAL
Skipping meta for .WAIT_1: .PHONY
Skipping meta for .dirdep: .NOMETA
Skipping meta for all: .MAKE
@ 1353259391 [2012-11-18 09:23:11] Checking /home/sjg/work/sjg/conf for i386 ...
/var/obj/sjg/i386/conf/config.status.meta: 2: a build command has changed
CC="cc " /home/sjg/work/sjg/conf/configure --no-create
vs
./config.status --recheck
Building /var/obj/sjg/i386/conf/config.status
...
/var/obj/sjg/i386/lib/sjg/globals.3.meta: 2: there were more build commands in the meta data file than there are now...
...
/var/obj/sjg/i386/lib/sjg/dbug.cat3.meta: 2: a build command has changed
@echo "nroff -man /home/sjg/work/sjg/lib/dbug/dbug.3 > dbug.cat3"
vs
@echo "nroff -man dbug.3 > dbug.cat3"
Building /var/obj/sjg/i386/lib/sjg/dbug.cat3
...

.MAKE.MODE

This variable is processed after all makefiles have been read, and can control the behavior of bmake. It can contain various words:

compat

puts bmake into compat mode (the -B command line option sets .MAKE.MODE=compat). Many makefiles are written with implicit dependencies which only work when targets are made in the order specified. These makefiles can be run in meta compat mode.

Some makefiles rely on stderr being separated from stdout. Such makefiles need to run in compat mode.

meta

puts bmake into meta mode. The following are only relevant in this case (any combination will do):

verbose
When generating or updating a .meta file, print the value of .MAKE.META.PREFIX. The default is Building ${.TARGET:H:tA}/${.TARGET:T}.
env
Include the environment in .meta files (see below). This can add a lot of volume, and isn't directly used - hence is optional.
nofilemon
Do not attempt to use filemon. For a one time clean tree build, there is no need to capture dependency information, though the system call activity, can still be valuable for debugging. Regardless, the .meta files are useful for capturing error output.
read
Read .meta files if they exist, but do not update them. This is more extreme than nofilemon and potentially a bad idea, since you lose the ability to capture error output.
ignore-cmd
Some makefiles have commands which are simply not stable. This tells bmake to not consider a target out-of-date due to a change of command. A change in the number of commands will still make the target out-of-date. The same effect can be had on a per target basis using the special source .NOMETA_CMP.

For example:

.MAKE.MODE = meta verbose
.MAKE.MODE = compat

some makefiles want to ensure they run in compat mode regardless of meta mode:

.MAKE.MODE += compat

can take care of that. In some cases, a makefile does not want to run in meta mode, but does not want to run in compat mode either:

.MAKE.MODE = normal
.MAKE.MODE =

the actual value in that case is unimportant (so long as it contains neither meta nor compat), I use normal.

filemon

This is a kernel module which wraps system calls that are of interest to make. It is a clone device, so each time it is opened a new instance is created. When building a target bmake opens filemon, and provides it a temp file to write to. When bmake forks a child, the child associates its pid with the relevant filemon device.

Then any of the wrapped system calls performed by the child (or its descendants) will be recorded in the temp file provided by bmake.

Filemon only records successful syscalls. This limits the data collected to only that which we are interested in.

Similar information could be obtained by ktrace(8), but ktrace captures all syscalls including those which fail, which makes it harder to gather the relevant data.

The prototype build (later jbuild) used DTrace for tracking the system calls of interest to the build. On FreeBSD at least however, this required root privs - which is not desirable. The filemon device avoids that, with less overhead.

For each syscall, an entry of the form:

tag pid pathname

is added, where tag is one of:

C       chdir
D       unlink
E       exec
F       [v]fork
L       [sym]link
M       rename
R       open for read
S       stat
W       open for write
X       exit

as noted below, the C E and R entries are of particular interest to bmake.

Extracting dependencies

While bmake itself simply uses .meta files to help evaluate the out-of-date status of targets, and to capture command output for diagnosis purposes, there is lots of useful data collected from filemon which can be easily leveraged.

For example, we can extract a list of all the files opened for reading. We can split these into two sets:

generated files

Any file that appears in the object tree of our sandbox, is a generated file, and by definition needs to be up to date before the current target is made.

If that generated file is not in the current object directory, we have detected a directory which needs to be visited before the current directory.

Ensuring that the layout with the object trees mirror that within the src tree is a trivial means of being able to map generated files back to their src directories.

src files

Any file read from the src tree is one that must exist for the current target to be built. If that file is outside of the current directory, then it represents a directory which must be present in the tree.

These src dependencies can be leveraged to drive minimal subset checkout logic.

Makefiles

The following sections provide some detail about some example makefiles that leverage meta mode to improve build performance and functionality.

Makefile.depend

The build and src dependencies can be collected as relative paths (from the top of the tree), into a generated file that can be checked into the SCM.

This is the most visible change, since every leaf directory gets one or more of these.

Note that the name Makefile.depend is just an example (though not a bad one ;-) it is just what I set as the value for .MAKE.DEPENDFILE.

The Junos build, is always cross-built for multiple architectures so there the value used for .MAKE.DEPENDFILE is Makefile.depend.${MACHINE}.

The per ${MACHINE} depend file, avoids the need for a mutex when updating, and avoids the need for any cleverness in representing machine specific paths in canonical forms (for example; replacing sys/i386/include with sys/${MACHINE_ARCH}/include.) The simplicity hopefully trumps the overhead of many almost identical small files.

For the FreeBSD build I use a plain Makefile.depend for most of the tree. The only architecture path to cater for is lib/csu/* which is mapped to lib/${CSU_DIR}. A very small number of directories generate machine specific local sources (which need to be captured in Makefile.depend see below), for these a Makefile.depend.${MACHINE} makes sense. Using sys.dependfile.mk makes it all just work.

In addition, we can extract local dependencies needed for a parallel build in a clean tree. That is; any file read from the current object directory is a local dependency for the target being made. If this information is being recorded in Makefile.depend, then it is wise to (if necessary) fake entries for profiled objects (.po) to avoid needless churn when some builds are done with profiling enabled and some are not.

For example, given the makefile:

PROG = ${.CURDIR:T}

SRCS = getdate.c  main.c

YACC ?= yacc
DELAY ?= 1

getdate.h:      getdate.y
        ${YACC} -d ${.ALLSRC:M*.y}
        mv y.tab.c $*.c
        sleep ${DELAY}
        mv y.tab.h $*.h

getdate.c: getdate.h

.include <bsd.prog.mk>

there is a missing dependency (assuming main.c includes getdate.h). Without addressing that, a bmake -j8 in a clean directory will likely fail. The sleep is just there to help exercise the race condition.

However, if we end up with something like the following in Makefile.depend:

# Autogenerated - do NOT edit!

DEP_RELDIR := ${.PARSEDIR:tA:S,${SRCTOP}/,,}

DIRDEPS = \
        lib/libc

.include <dirdeps.mk>

.if ${DEP_RELDIR} == ${_DEP_RELDIR}
# local dependencies - needed for -jN in clean tree
main.o: getdate.h
main.po: getdate.h
.endif

The missing dependency is taken care of.

Obviously, if the programmer was smart enough to get the dependencies for getdate.c right, he likely wouldn't have missed that for main.o but not all makefiles are this simple, and the dependency on getdate.h may have been added later.

One build product per directory

Actually that is an exaggeration. There is no problem with building multiple things (like the various flavors of lib that bsd.lib.mk generates), so long as the behavior is consistent.

Since Makefile.depend* are intended to be checked into the SCM, they should be stable. For most makefiles this is a non-issue.

To avoid capturing spurious dependencies meta.autodep.mk only considers updating Makefile.depend* if we successfully built the default or all target. Thus doing bmake cscope or bmake etags does not perturb the captured dependencies.

Given the above, makefiles which build multiple things, by use of sub-makes running in the same directory for different targets, will not have all their dependencies captured correctly. Re-organizing such cases to have sub-dirs per build product such that each can simply be built via the all target solves that problem.

meta.autodep.mk

Since the extraction of build dependencies from the .meta files, is controlled by the makefiles (if done at all), it is desirable to avoid running that process unnecessarily.

The fact that bmake tracks updated .meta files via .MAKE.META.CREATED, makes it possible to optimize the updating of dependencies, and a much simpler autodep.mk than needed when using gcc -M*:

.END:           gendirdeps

_DEPENDFILE := ${.CURDIR}/${.MAKE.DEPENDFILE:T}
gendirdeps:     ${_DEPENDFILE}

# the double $$ defers initial evaluation
${_DEPENDFILE}: $${.MAKE.META.CREATED} ${.PARSEDIR}/gendirdeps.mk
        @echo Updating $@: ${.OODATE:T:[1..8]}
        @(cd ${.CURDIR} && \
        SKIP_DIRDEPS='${SKIP_DIRDEPS:O:u}' \
        ${.MAKE} __objdir=${_OBJDIR} -f gendirdeps.mk $@ \
        META_FILES='${.MAKE.META.FILES:T:O:u}' )

As noted the double $$ in the dependency line, prevents .MAKE.META.CREATED being expanded immediately, which works to our advantage. Also note: we use .MAKE.META.CREATED only to know that an update is needed, .MAKE.META.FILES is what we use for the update though.:

$ vi cat.c
$ bmake
Checking /c/sjg/work/sb/src/bsd/gnu/lib/csu for i386 ...
Checking /c/sjg/work/sb/src/bsd/lib/csu/i386-elf for i386 ...
Checking /c/sjg/work/sb/src/bsd/include for i386 ...
Checking /c/sjg/work/sb/src/bsd/usr.bin/rpcgen for host ...
Checking /c/sjg/work/sb/src/bsd/include/rpc for i386 ...
Checking /c/sjg/work/sb/src/bsd/include/rpcsvc for i386 ...
Checking /c/sjg/work/sb/src/bsd/lib/libc for i386 ...
Building /c/sjg/work/sb/obj-i386/bsd/bin/cat/cat.o
Building /c/sjg/work/sb/obj-i386/bsd/bin/cat/cat
Updating /c/sjg/work/sb/src/bsd/bin/cat/Makefile.depend.i386: cat.o.meta cat.meta
$ bmake
Checking /c/sjg/work/sb/src/bsd/gnu/lib/csu for i386 ...
Checking /c/sjg/work/sb/src/bsd/lib/csu/i386-elf for i386 ...
Checking /c/sjg/work/sb/src/bsd/include for i386 ...
Checking /c/sjg/work/sb/src/bsd/usr.bin/rpcgen for host ...
Checking /c/sjg/work/sb/src/bsd/include/rpc for i386 ...
Checking /c/sjg/work/sb/src/bsd/include/rpcsvc for i386 ...
Checking /c/sjg/work/sb/src/bsd/lib/libc for i386 ...
$

This model also works independently of the tool-chains being used, whereas gcc -M* requires use of -MF and -MT to do a decent job. Not to mention other languages.

If you know that the rest of the tree is up to date, you can tell dirdeps.mk to skip checking:

$ vi cat.c
$ bmake -DNO_DIRDEPS
Building /c/sjg/work/sb/obj-i386/bsd/bin/cat/cat.o
Building /c/sjg/work/sb/obj-i386/bsd/bin/cat/cat
Updating /c/sjg/work/sb/src/bsd/bin/cat/Makefile.depend.i386: cat.o.meta cat.meta
$ bmake -DNO_DIRDEPS
$

gendirdeps.mk

This is the makefile which extracts DIRDEPS and optionally SRC_DIRDEPS from a bunch of .meta files. To do this, it runs meta2deps with a list of dirctories prefixes - generally the same as set in .MAKE.META_BAILIWICK, and a list of .meta files - which as noted above bmake keeps track of.

Sometimes we do not feed all the .meta files to meta2deps, for example, a library that builds .o, .So and .po objects from the same set of srcs. If the number of srcs is large we default to only processing the .So.meta files.

meta2deps

The original implementation was a shell script, but a Python version is used if possible - being much faster, and provides for better debug capabilites.

It is provided SRCTOP and one or more OBJTOP variables as well as corresponding OBJROOT values. For example if OBJTOP=/var/obj/i386, then OBJROOT=/var/obj/.

The goal is for meta2deps to identify files that are within those top level directories - and hence of interest, and be able to map objdirs, to ${RELDIR}.${MACHINE} within the src tree. RELDIR is the relative path from SRCTOP.

In general one can use :S,${OBJTOP},${SRCTOP}, to map an object directory to the corresponding src directory, and :S,${SRCTOP}/,, to convert that to a tree relative path (RELDIR) - which is what we want in DIRDEPS etc.

If generated files are collected together in a common directory (eg. ${OBJROOT}/common/include/) the mapping described above can fail. By putting beside each such file a file.dirdep which contains the correct RELDIR value this problem is solved.

dirdeps.mk

All of the logic for dealing with DIRDEPS is encapsulated in dirdeps.mk which is included by Makefile.depend.${MACHINE}, and is only relevant for the initial instance of bmake (${.MAKE.LEVEL} == 0).

Conceptually, the process is quite simple (even if the implementation is not):

When the initial bmake reads ${.CURDIR}/Makefile.depend.${MACHINE}, it gets an initial set of DIRDEPS.

dirdeps.mk transforms DIRDEPS into a set of absolute paths with a .${MACHINE} suffix, to deal with building for multiple machine types. The pseudo machine .host represents the build host. These are hooked into a dependency for the target dirdeps.

dirdeps.mk also turns DIRDEPS into a list of */Makefile.depend* files which will be read, to get more DIRDEPS.

Since each Makefile.depend* includes dirdeps.mk the process is recursive, at each point adding something like:

${SRCTOP}/${DEP_RELDIR}.${MACHINE}: ${DIRDEPS:@d@${SRCTOP}/$d.${MACHINE}@}

to the graph.

It is actually more complicated than that, to deal with cases where ${DEP_RELDIR}.${MACHINE} depends on dirs built for other machines (eg. pseudo machines like host for host tools). But the above shows how a tree wide set of dependencies are built.

All the expanded DIRDEPS are associated with a build macro which will cause them to be visited with MACHINE set to the correct value:

# we suppress SUBDIR when visiting the leaves
_DIRDEP_USE:    .USE .MAKE
        @for m in ${.MAKE.MAKEFILE_PREFERENCE}; do \
                test -s ${.TARGET:R}/$$m || continue; \
                echo "${TRACER}Checking ${.TARGET:R} for ${.TARGET:E} ..."; \
                MACHINE=${.TARGET:E} MACHINE_ARCH= NO_SUBDIR=1 \
                ${.MAKE} -C ${.TARGET:R} || exit 1; \
                break; \
        done

Regardless of whether automated dependency handling is an attractive idea, the combination of dirdeps.mk and Makefile.depend* provides a simple and effective means of handling a mixture of directory and machine dependencies.

meta.subdir.mk

While our goal is to build by visiting the tree's leaf nodes directly, we still need to be able to launch a build in say src/lib/ to build all the libraries there - perhaps to check we didn't break any.

If there is no Makefile.depend in the current directory, meta.subdir.mk does something like:

_subdirs != find ${SUBDIR} -name 'Makefile.depend*'

and assigns the cleaned up result to DIRDEPS. Thus the initial DIRDEPS includes all the leaf directories below the current one. Including dirdeps.mk does the rest.

sys.dependfile.mk

This makefile is included during the sys.mk phase. It processes .MAKE.DEPENDFILE_PREFERNCE. It allows a tree to contain a combination of MACHINE dependent and independent depend files, it provides defaults:

# All depend file names should start with this
.MAKE.DEPENDFILE_PREFIX ?= Makefile.depend

# The order of preference: we will use the first one of these we find
# otherwise the 1st entry will be used by default.
.MAKE.DEPENDFILE_PREFERENCE ?= \
        ${.CURDIR}/${.MAKE.DEPENDFILE_PREFIX}.${MACHINE} \
        ${.CURDIR}/${.MAKE.DEPENDFILE_PREFIX}

actually if nothing is found it does not necessarily use the first entry. If the first entry is not machine dependent (does not end in ${MACHINE}) but at least one entry does, it will look to see if any machine dependent files exist, and if so it will pick the first machine dependent entry to use.

meta.stage.mk

This makefile allows for automatically installing files during the build, but for each file to be installed, it also places a file.dirdep in the destination directory.

This .dirdep file, records who put the file there ${RELDIR}.${MACHINE}, and is used by meta2deps to simplify the tracing of dependencies back to src directory when the normal heuristics do not work.


Revision:$Id: bmake-meta-mode.txt,v 1.6 2022/04/23 21:49:02 sjg Exp $