require’s build process¶
Note
This document assumes a familiarity with GNU make. See
GNU Make manual - comprehensive reference
Make tutorial - practical introduction
The require build process is a complicated bit of work. Note that this is in addition to the conda build process (see Building modules); we will assume that you are comfortable with that process and are interested in learning about the internals of the require-specific build process.
In general the build scripts for e3 modules built with require will contain the following steps (see Build Targets):
make MODULE=${PKG_NAME} LIBVERSION=${PKG_VERSION}
make MODULE=${PKG_NAME} LIBVERSION=${PKG_VERSION} install
As stated in the section on Module build configurations,
the recipe’s included Makefile must begin with
include $(E3_REQUIRE_TOOLS)/driver.makefile
Recall that this script and Makefile are located in the source directory after all sources have been unpacked and patched. This is the directory in which the build script runs.
The make process for require¶
Overview¶
We start in the source directory and run
make MODULE=${PKG_NAME} LIBVERSION=${PKG_VERSION} target
Note
Note that MODULE must be provided for many of the build targets, as this is
used to provide the build system with the location of the target build.
Regardless of the target, the build process runs several successive passes
(using recursive make) in order to collect all of the necessary information.
These are:
Collect initial information and determine target architecture
Determine architecture-specific information (e.g. sources, configuration)
Perform the appropriate build/install/whatever task
The first two steps take place in the source directory, and the final step
takes place in a generated build directory O.${EPICSVERSION}_${T_A}.
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.
Stage 1: The source directory¶
On the first pass in the source directory we collect architecture-independent
information. This includes most source files (the ones that do not depend on the
architecture), header files, scripts, and snippets. We also determine which
architectures to build for depending on the module-specific configuration (e.g.
EXCLUDE_ARCH).
We also load all of the configuration from EPICS base:
EB:=${EPICS_BASE}
-include ${CONFIG}/CONFIG
EPICS_BASE:=${EB}
Note
The redefinition of EPICS_BASE is due to the fact that it is overwritten in
CONFIG_SITE from EPICS base; see
here
and here
This redefinition depends on your build configuration, and exists to guard against
installations that use INSTALL_LOCATION to relocate their EPICS installation.
The sources and other files are handled roughly as follows:
SRCS = ${SOURCES}
export SRCS
which takes the variable SOURCES from the module build configuration and passes
it on to future rounds of the build process as the “internal” variable SRCS.
Note
SRCS matches the variables in the build configuration from EPICS base
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 exported. 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
require build process), it will export correctly.
Warning
There is some complexity and subtlety here. The first part is that we must be careful when using variable names that overlap with the build rules from EPICS base as when we load the EPICS build configuration, we will trigger some build rules if we populate matching variables.
The second point is that, given that Makefiles interacting with require will
be written:
SOME_VAR += SOME_VALUE
If we are not careful and we were to simply export SOME_VAR (instead of
modifying the variable name), then on each further recursive call to make we
would append SOME_VALUE again to SOME_VAR; whether or not this is a problem
depends entirely on what exactly happens to that value later on in the build
process. As such, the simplest thing to do here is to modify the interface
variable, such as mapping SOURCES to SRCS.
Once we have all of the exports sorted, we recursively call make and move onto
the next round. This next stage is triggered by the following:
define target_rule
$1-%:
$${MAKE} -f $${USERMAKEFILE} T_A=$$* $1
endef
$(foreach target,$(RECURSE_TARGETS),$(eval $(call target_rule,$(target))))
.SECONDEXPANSION:
$(foreach target,$(RECURSE_TARGETS),$(eval $(target): $$$$(foreach arch,$$$${BUILD_ARCHS},$(target)-$$$${arch})))
We can simplify this by focusing purely on the build target (one of the
RECURSE_TARGETS); 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 (the target architecture) set
appropriately.
Note
.SECONDEXPANSION is used here for the following reason: the architecture
filters are defined after the inclusion of driver.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 architectures.
Stage 2: Preparing to build T_A¶
For this stage of the build process, we are still in the source 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
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
This 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 any other part of
VAR_EXTENSIONS) to selectively compile code based on architecture and
version.
Finally, we run:
$(RECURSE_TARGETS): 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 3: 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
One way of thinking of this multi-stage process is that the first two passes tell the build system what to build, while this pass tells it how to build. Some specific details follow; for examples see the next section.
We determine where all of the install paths will be via
INSTALL_REV = ${MODULE_LOCATION} INSTALL_BIN = ${INSTALL_PREFIX}/bin INSTALL_LIB = ${INSTALL_PREFIX}/lib INSTALL_INCLUDE = ${INSTALL_PREFIX}/include INSTALL_DBD = ${INSTALL_REV}/dbd INSTALL_DB = ${INSTALL_REV}/db INSTALL_CONFIG = ${INSTALL_REV}/cfg INSTALL_DOC = ${INSTALL_REV}/doc INSTALL_SCR = ${INSTALL_REV}
Note
Note that unlike traditional EPICS build systems, we install binaries, libraries, and headers at the root level of the install location so that they are more readily found on
PATH.In this section we heavily use the
vpathdirective to help determine the source of the files that need to be compiled and/or installedIn order to manage dependencies chains within e3, we inject a custom source file into every module which runs the registration functions necessary for the module (
init.cpp)Additional include paths for header files are set here (more generally, this is where we set all the compilation and linking flags). For example:
SRC_INCLUDES = $(addprefix -I, $(wildcard $(foreach d,$(call uniq, $(filter-out /%,$(dir ${SRCS:%=../%} ${HDRS:%=../%}))), $d $(addprefix $d/, os/${OS_CLASS} $(POSIX_$(POSIX)) os/default))))
This adds the path of every source and header file to the search path when compiling source files.
There are of course other details. In general this is one of the most complicated parts of the require build process; the details are mainly useful when debugging various build or install issues.
Examples of the make process¶
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.
Header files are installed by adding the line
HEADERS += header.h to your Makefile. This is then handled by the install
target in your makefile. This process then runs as follows.
Note
There are a few header files that can be installed via other means; for example, if you defined device support, then the auto-generated headers are automatically installed as well.
In stage 1 we start with the following:
HDRS = ${HEADERS} HDRS += $(RECORDS:%=${COMMON_DIR}/%.h) 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)As described in stage 3, the variable
HDRSis used within the directoryO.${EPICSVERSION}_${T_A}: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.hdepends onheader.hFinally, the
vpathline above tellsmakewhere to search for that file, and then the instructions tellmaketo run the program defined by$(INSTALL)to install the file in the target location.Warning
Note that there is one potential source of problems here: the dependency is just the filename alone. Thus if you would like to install both of the header files
dir1/header.hdir2/header.h(with the same filename but different paths), then only one of these two will be installed. See the build interface for documentation on this case.
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 Makefile.
The next steps are complicated due to being shared among different configure files.
Initially in stage 1 above, we have the line
SRCS = ${SOURCES}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.cintofile.din the variableHDEPENDS_FILES.Next in stage 3, we include
RULESfrom 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.dfromfile.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 3 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.otoLIBRARY_OBJS.Next, we look at
LOADABLE_SHRLIBNAME: roughly speaking, if you end up with a non-emptyLIBRARY_OBJS(as we have above), thenLOADABLE_SHRLIBNAMEevaluates tolib${PRJ}.so. In particular, we obtain fromRULES_BUILDthe 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_BUILDwe 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
builddepends 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.This finally allows us to build our shared library; since
file.owas already created when we generatedfile.d, we can run the linking command in order to obtain our shared library, ready to install.