Article: The build process with require (In-depth; advanced)¶
The e3 build process is a complicated bit of work. To recap, the overview is as follows:
In the e3-wrapper directory: we collect some information and decide what build process we will perform (from
RULES_E3
), callingmake
in the module directory with information passed as inCONFIG_E3_MAKEFILE
.EPICSVERSION
is determined by the pathEPICS_BASE
.In the module directory: Target architecture
${T_A}
has not been defined, so determine the architectures to build for.In the module directory: Perform a final collection of the relevant files, create the directories
O.${EPICSVERSION}_Common
andO.${EPICSVERSION}_${T_A}
.In the directories
O.*
: Build/Install all of the required shared libraries and other files for the given version of EPICS base and target architecture.
We will go over each of these steps in more detail, as well as go over an example build to explain how information is collected and used by the build process.
Some details about make
¶
Before we can describe the build process, we have to talk a little about how
make
works. If you do not understand make
, it is very very hard to
understand the e3 build process. For a reference, I can suggest GNU make
reference.
In short, make
does two things:
It provides a framework to describe the tree structure of dependencies of a given project
It provides a way to give instructions how to build those dependencies if they are missing or out of date
These are built up of instructions that look like
VARIABLE = value
target: dependency
#actions
echo $(VARIABLE)
If you ran the command make target
it would check first that dependency
is
up-to-date (i.e. newer than target
), and if it is, it would run the commands
below.
$ make target
echo value
value
The key is in how make
generates its dependency tree. Unlike many programming
languages (of which make
is… not necessarily one?), make
is decidedly
non-procedural: since lines are not evaluated and actions are not performed in a
linear order, it can be very difficult to trace exactly how a variable has
obtained its value, or why certain actions have been performed.
The make
process for e3¶
Stage 1: The e3-wrapper¶
We start in the e3-wrapper directory, and run (for example) make build
. The
first thing that happens is that we load the makefiles from the configure
directory; these in turn load CONFIG_MODULE
and RELEASE
which specify
dependencies and for which version of EPICS base and require we are building,
as well as CONFIG
from the require module (located in
configure/modules/CONFIG
in this repository).
We also load RULES
which similarly loads a number of rules-related configure
files installed with require
. The most important one in RULES_E3
which
initiates most of the e3 build process. As an example, we have:
## Build the EPICS Module : $(E3_MODULE_NAME)
# Build always the Module with the EPICS_MODULES_TAG
build: conf checkout
$(QUIET) $(E3_MODULE_MAKE_CMDS) build
which first makes sure that conf
and checkout
are up to date (these copy the
$(module).Makefile
into the module directory, and run a git checkout
command
to make sure that the module is up-to-date). Note that $(E3_MODULE_MAKE_CMDS)
is defined in CONFIG_E3_MAKEFILE
which specifies which arguments should be
passed to this recursive call of make
.
Stage 2: Defining T_A
¶
In e3, we only build for a single verion of EPICS base at a time. This is defined
in driver.makefile
as
EPICSVERSION:=$(patsubst base-%,%,$(notdir $(EPICS_LOCATION)))
which converts, for example, /opt/epics/base-7.0.6.1
into 7.0.6.1
.
We begin by determining the target architectures to build for. In this case,
we may build for more than one architecture at a time; at the moment, ESS
supports linux-x86_64
, linux-corei7-poky
, and linux-ppc64e6500
(as well
as a debug architecture, linux-x86_64-debug
). This is also where we include the
EPICS build rules: see the sequence
EB:=${EPICS_BASE}
-include ${CONFIG}/CONFIG
EPICS_BASE:=${EB}
(The redefinition of EPICS_BASE
is due to the fact that it is overwritten in
CONFIG_SITE
from EPICS base)
This is also the place where we start collecting information about what to build and install. For example, to begin collecting the source files to compile, we have the following section:
AUTOSRCS := $(filter-out ~%,$(wildcard *.c *.cc *.cpp *.st *.stt *.gt))
SRCS = $(if ${SOURCES},$(filter-out -none-,${SOURCES}),${AUTOSRCS})
export SRCS
Note in particular the export SRCS
line: when make is called recursively,
variables from one run to the next do not persist unless they are export
ed. It
is also extremely important to note when the variable being exported is
expanded: this happens right before the next iteration of recursive make
is
called, so even if SOURCES
will only be defined later (as is the case with the
e3 build process), it will export
correctly.
The cross-compiler target architectures CROSS_COMPILER_TARGET_ARCHS
are
defined in $(EPICS_LOCATION)/configure/CONFIG_SITE
, which is generated when
you build EPICS base for the first time.
The next stage of the build is triggered by
define target_rule
$1-%: | $(COMMON_DIR)
$${MAKE} -f $${USERMAKEFILE} T_A=$$* $1
endef
$(foreach target,install build debug,$(eval $(call target_rule,$(target))))
.SECONDEXPANSION:
$(foreach target,install build debug,$(eval $(target):: $$$$(foreach arch,$$$${BUILD_ARCHS},$(target)-$$$${arch})))
We can simplify this by focusing purely on the build target; in that case this essentially reads
build-%: | $(COMMON_DIR)
${MAKE} -f ${USERMAKEFILE} T_A=$* build
.SECONDEXPANSION:
build:: $$(foreach arch,$${BUILD_ARCHS},$(target)-$${arch})
i.e. build
depends on build-T_A_1
, build-T_A_2
, etc., each of which trigger
a call to run make build
again with T_A
set appropriately.1
Stage 3: Preparing to build T_A
¶
For this stage of the build process, we are still in the module directory; the
next stages will be done in the directories O.$(EPICSVERSION)_Common
or
O.$(EPICSVERSION)_$(T_A)
, respectively. These directories will also be created
at this point, and are the destination of all intermediate and final output
files (e.g. any generated .db
or .dbd
files, .o
files, and
lib$(module).so
)
Note that make clean
simply deletes these directories, removing all generated
files.
We make a final collection of what objects we should build, and a final gathering of information:
# Add sources for specific epics types or architectures.
ARCH_PARTS = ${T_A} $(subst -, ,${T_A}) ${OS_CLASS}
VAR_EXTENSIONS = ${EPICSVERSION} ${ARCH_PARTS} ${ARCH_PARTS:%=${EPICSVERSION}_%}
export VAR_EXTENSIONS
allows the developer to have architecture-specific files: for example, if
T_A = linux-x86_64
then ARCH_PARTS
will be linux-x86_64 linux x86_64
:
If we now consider the next segment, we see
SRCS += $(foreach x, ${VAR_EXTENSIONS}, ${SOURCES_$x})
USR_LIBOBJS += ${LIBOBJS} $(foreach x,${VAR_EXTENSIONS},${LIBOBJS_$x})
export USR_LIBOBJS
which tells us that we can have SOURCES_x86_64
(or another other part of
VAR_EXTENSIONS
) to selectively compile code based on architecture and
version.
Finally, we run
install build debug:: O.${EPICSVERSION}_Common O.${EPICSVERSION}_${T_A}
@${MAKE} -C O.${EPICSVERSION}_${T_A} -f ../${USERMAKEFILE} $@
Note that due to the argument -C O.${EPICSVERSION}_${T_A}
we switch to that
directory, using the same ${USERMAKEFILE}
to manage the build process.
Stage 4: Building T_A
¶
We have now collected the majority of the information that we need to build our
module. We will do a little more organisation and preparation, and then the
process will be handed over to the EPICS build system. Note that this part of
driver.makefile
is by far the most complicated section, and takes some time to
digest.
To begin with, I would like to point out a couple sections of interest, followed
by tracing through what happens when you include a line such as SOURCES += file.c
in your $(module).Makefile
.
Examples of the make
process¶
We will provide a few examples of how make
processes the data and produces the
desired result. The first is installing a header file, and the second is
actually compiling a source file.
Installing a header file¶
Before we go on to the more complicated case of compiling source files, let us
go over the simpler step of having header files be installed so that other
modules may include them. As an example, there are many .h
files that are
installed with asyn and are used by lots of other modules.
The simplest way of including a header file is to add the line HEADERS += header.h
into your $(module).Makefile
. Having done this, the build/install
process runs as follows.
In stage 2 we start with the following:
HDRS = ${HEADERS} $(addprefix ${COMMON_DIR}/,$(addsuffix Record.h,${RECORDS})) HDRS += ${HEADERS_${EPICSVERSION}} export HDRS
which passes these on to the variable
HDRS
(as well as collecting a few other headers, including version-specific ones if necessary)There is only one place in the build process that these are relevant: in stage 4 (within the directory
O.${EPICSVERSION}_{T_A}
) we have the following line:SRC_INCLUDES = $(addprefix -I, $(wildcard $(foreach d,$(call uniq, $(filter-out /%,$(dir ${SRCS:%=../%} ${HDRS:%=../%}))), $d $(addprefix $d/, os/${OS_CLASS} $(POSIX_$(POSIX)) os/default))))
or, simplified:
SRC_INCLUDES = $(addprefix -I, $(wildcard $(call uniq, $(filter-out /%,$(dir ${HDRS:%=../%})))))
which adds the directory that the header files are located in to the search path for include files when compiling.
The next time the headers come up is during the install process, and are governed by the following:
vpath %.h $(addprefix ../,$(sort $(dir $(filter-out /%,${HDRS}) ${SRCS}))) $(sort $(dir $(filter /%,${HDRS}))) # snip INSTALL_HDRS = $(addprefix ${INSTALL_INCLUDE}/,$(notdir ${HDRS})) # snip INSTALLS += ... ${INSTALL_HDRS} ... install: ${INSTALLS}
and the following from EPICS base
RULES_BUILD
:$(INSTALL_INCLUDE)/%: % $(ECHO) "Installing generic include file $@" @$(INSTALL) -d -m $(INSTALL_PERMISSIONS) $< $(@D)
which says that any target within the directory
$(INSTALL_INCLUDE)
has the target as a dependency, i.e.$(INSTALL_INCLUDE)/header.h
depends onheader.h
Finally, the
vpath
line above tellsmake
where to search for that file, and then the instructions tellmake
to run the program defined by$(INSTALL)
to install the file in the target location. Note however that there is one potential source of problems here: the dependency is just the filename alone, and so if you have the following two header files you would like to include:dir1/header.h
dir2/header.h
i.e. the same filename, but different locations, then only one of these two will be installed.In order to avoid this, you can add a path to the variable
KEEP_HEADER_SUBDIRS
, which will preserve the directory tree structure of headers under that path.
Compiling a .c
file¶
Building source files at its heart is similar to the above, but the chain of
dependencies is significantly more complicated. As above however, the inclusion
of a source file to be compiled into the shared library is simple: add the line
SOURCES += $(APPSRC)/file.c
in your module makefile.
The next steps are complicated due to being shared among different configure files.
Initially in stage 2 above, we have the line
SRCS += $(if ${SOURCES},$(filter-out -none-,${SOURCES}),${AUTOSRCS})
which includes your file in the variableSRCS
.In the EPICS base configure file
CONFIG_COMMON
, we have the following two directives:SRC_FILES = $(LIB_SRCS) $(LIBSRCS) $(SRCS) $(USR_SRCS) $(PROD_SRCS) $(TARGET_SRCS) HDEPENDS_FILES = $(addsuffix $(DEP),$(notdir $(basename $(SRC_FILES))))
which converts
$(APPSRC)/file.c
intofile.d
in the variableHDEPENDS_FILES
.Next in stage 4, we include
RULES
from EPICS base which includesRULES_BUILD
. This includes the following:-include $(HDEPENDS_FILES)
which seems quite innocuous, but it is a surprisingly important line:
make
, when trying to include a file, will first see if it exists, and if it does not, then it will see if it can generate that file. In this case, we have the rule%$(DEP):%.c @$(RM) $@ $(HDEPENDS.c) $<
which provides a rule to create
file.d
fromfile.c
: this runs (once again, fromCONFIG_COMMON
):HDEPENDS_COMP.c = $(COMPILE.c) $(HDEPENDS_COMPFLAGS) $(HDEPENDS_ARCHFLAGS)
i.e. it compiles the source file with a special flag that produces not only
file.o
, but a dependency filefile.d
.To wit, on our first pass through in stage 5 we compile all of our source files to produce object files and dependency files.
We now need to connect the source files to the final shared library. The first step is the following from
driver.makefile
:LIBRARY_OBJS = $(strip ${LIBOBJS} $(foreach l,${USR_LIBOBJS},$(addprefix ../,$(filter-out /%,$l))$(filter /%,$l))) LIBOBJS += $(addsuffix $(OBJ),$(notdir $(basename $(filter-out %.$(OBJ) %$(LIB_SUFFIX),$(sort ${SRCS})))))
which adds
file.o
toLIBRARY_OBJS
.Next, we look at
LOADABLE_SHRLIBNAME
: roughly speaking, if you end up with a non-emptyLIBRARY_OBJS
(as we have above), then this will belib${PRJ}.so
. In particular, we obtain fromRULES_BUILD
the dependency and build rules:$(LOADABLE_SHRLIBNAME): $(LIBRARY_OBJS) $(LIBRARY_RESS) $(SHRLIB_DEPLIBS) $(LOADABLE_SHRLIBNAME): $(LOADABLE_SHRLIB_PREFIX)%$(LOADABLE_SHRLIB_SUFFIX): @$(RM) $@ $(LINK.shrlib) $(MT_DLL_COMMAND)
where the linking command is provided in
CONFIG.Common.UnixCommon
:LINK.shrlib = $(CCC) -o $@ $(TARGET_LIB_LDFLAGS) $(SHRLIBDIR_LDFLAGS) $(LDFLAGS) LINK.shrlib += $(LIB_LDFLAGS) $(LIBRARY_LD_OBJS) $(LIBRARY_LD_RESS) $(SHRLIB_LDLIBS)
Last but not least, we need to connect this to the target
build
. InRULES_BUILD
we find:LIBTARGETS += $(LIBNAME) $(INSTALL_LIBS) $(TESTLIBNAME) \ $(SHRLIBNAME) $(INSTALL_SHRLIBS) $(TESTSHRLIBNAME) \ $(DLLSTUB_LIBNAME) $(INSTALL_DLLSTUB_LIBS) $(TESTDLLSTUB_LIBNAME) \ $(LOADABLE_SHRLIBNAME) $(INSTALL_LOADABLE_SHRLIBS) # snip snip build: $(OBJSNAME) $(LIBTARGETS) $(PRODTARGETS) $(TESTPRODTARGETS) \ $(TARGETS) $(TESTSCRIPTS) $(INSTALL_LIB_INSTALLS)
and in particular, that
build
depends on$(LOADABLE_SHRLIBNAME)
.Putting this all together, we have the following chain of dependencies:
build -> $(LOADABLE_SHRLIBNAME) -> $(LIBRARY_OBJS)
where that last target includes
file.o
.The magic now comes from the fact that we have already built this file back when we were creating
file.d
! As such, we can run the linking command, and we obtain our shared library, ready to install.
- 1
Why do we need the
.SECONDEXPANSION
? The issue at hand is because the architecture filters are defined after the inclusion ofdriver.makefile
. As such, we take advantage of GNU make’s ability to do a deferred secondary expansion of target dependencies to ensure that we perform the correct filtering on archtectures.