7. Understanding module dependence

Lesson Overview

In this lesson, you will learn how to do the following:

  • Understand the environment variables used to manage dependencies

  • Understand how dependencies are determined at build-time

  • Install a new module version with different dependencies

  • Understand how incompatible dependencies can cause issues when loading an IOC


Dependent environment variables

Note

The versions of the modules mentioned below may be different that you will see in your environment, because the e3 tool that installs the modules packs gets the latest version of each module.

One of the key ideas with e3 (arguably, the key idea) is the management of dependencies of a given module in a common, structured way. That is, if a module A depends on a module B, then you should only need to load A; module B should be loaded and handled automatically (and so on, recursively). There are a few pieces that manage this.

Begin by switching to e3-stream/. Run make vars, and look at the variables *DEP_VERSION in the output:

[iocuser@host:e3-stream]$ make vars | grep DEP_VERSION
ASYN_DEP_VERSION = 4.42.0
CALC_DEP_VERSION = 3.7.4+1
EXPORT_VARS = E3_MODULES_VENDOR_LIBS_LOCATION E3_MODULES_INSTALL_LOCATION_LIB TEMP_CELL_PATH EPICS_HOST_ARCH EPICS_BASE MSI E3_MODULE_NAME E3_MODULE_VERSION E3_SITEMODS_PATH E3_SITEAPPS_PATH E3_SITELIBS_PATH E3_REQUIRE_MAKEFILE_INPUT_OPTIONS E3_REQUIRE_NAME E3_REQUIRE_CONFIG E3_REQUIRE_DB E3_REQUIRE_LOCATION E3_REQUIRE_DBD E3_REQUIRE_VERSION E3_REQUIRE_TOOLS E3_REQUIRE_INC E3_REQUIRE_LIB E3_REQUIRE_BIN QUIET PCRE_DEP_VERSION CALC_DEP_VERSION ASYN_DEP_VERSION
PCRE_DEP_VERSION = 8.44.0

These variables are defined and used, respectively, in configure/CONFIG_MODULE and StreamDevice.Makefile.

[iocuser@host:e3-stream]$ cat configure/CONFIG_MODULE | grep DEP_VERSION
ASYN_DEP_VERSION:=4.42.0
PCRE_DEP_VERSION:=8.44.0
CALC_DEP_VERSION:=3.7.4+1
[iocuser@host:e3-stream]$ cat StreamDevice.Makefile  | grep DEP_VERSION
ifneq ($(strip $(ASYN_DEP_VERSION)),)
asyn_VERSION=$(ASYN_DEP_VERSION)
ifneq ($(strip $(PCRE_DEP_VERSION)),)
pcre_VERSION=$(PCRE_DEP_VERSION)
ifneq ($(strip $(CALC_DEP_VERSION)),)
calc_VERSION=$(CALC_DEP_VERSION)

Let us see where these variables are used. Run make build and look for (something like) the following in the output.

make[4]: Entering directory `/home/simonrose/data/git/e3/modules/core/e3-stream/StreamDevice/O.7.0.6.1_linux-x86_64'
/usr/bin/gcc  -D_GNU_SOURCE -D_DEFAULT_SOURCE         -DUSE_TYPED_RSET            -D_X86_64_  -DUNIX  -Dlinux             -MD   -O3 -g   -Wall -Werror-implicit-function-declaration               -mtune=generic      -m64 -fPIC           -I. -I../src/ -I.././src/   -I/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include                -I/epics/base-7.0.6.1/require/4.0.0/siteMods/pcre/8.44.0+0/include            -I/epics/base-7.0.6.1/require/4.0.0/siteMods/calc/3.7.4+0/include  -I/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include                -I/epics/base-7.0.6.1/require/4.0.0/siteMods/pcre/8.44.0+0/include            -I/epics/base-7.0.6.1/require/4.0.0/siteMods/calc/3.7.4+0/include -I/epics/base-7.0.6.1/include  -I/epics/base-7.0.6.1/include/compiler/gcc -I/epics/base-7.0.6.1/include/os/Linux                           -c  .././src/StreamVersion.c
/usr/bin/g++  -D_GNU_SOURCE -D_DEFAULT_SOURCE         -DUSE_TYPED_RSET            -D_X86_64_  -DUNIX  -Dlinux             -MD   -O3 -g   -Wall         -std=c++11     -std=c++11  -mtune=generic               -m64 -fPIC          -I. -I../src/ -I.././src/   -I/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include                -I/epics/base-7.0.6.1/require/4.0.0/siteMods/pcre/8.44.0+0/include            -I/epics/base-7.0.6.1/require/4.0.0/siteMods/calc/3.7.4+0/include  -I/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include                -I/epics/base-7.0.6.1/require/4.0.0/siteMods/pcre/8.44.0+0/include            -I/epics/base-7.0.6.1/require/4.0.0/siteMods/calc/3.7.4+0/include -I/epics/base-7.0.6.1/include  -I/epics/base-7.0.6.1/include/compiler/gcc -I/epics/base-7.0.6.1/include/os/Linux                           -c  ../src/StreamFormatConverter.cc
/usr/bin/g++  -D_GNU_SOURCE -D_DEFAULT_SOURCE         -DUSE_TYPED_RSET            -D_X86_64_  -DUNIX  -Dlinux             -MD   -O3 -g   -Wall         -std=c++11     -std=c++11  -mtune=generic               -m64 -fPIC          -I. -I../src/ -I.././src/   -I/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include                -I/epics/base-7.0.6.1/require/4.0.0/siteMods/pcre/8.44.0+0/include            -I/epics/base-7.0.6.1/require/4.0.0/siteMods/calc/3.7.4+0/include  -I/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include                -I/epics/base-7.0.6.1/require/4.0.0/siteMods/pcre/8.44.0+0/include            -I/epics/base-7.0.6.1/require/4.0.0/siteMods/calc/3.7.4+0/include -I/epics/base-7.0.6.1/include  -I/epics/base-7.0.6.1/include/compiler/gcc -I/epics/base-7.0.6.1/include/os/Linux                           -c  ../src/StreamProtocol.cc

In particular, note the following segments:

-I/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include
-I/epics/base-7.0.6.1/require/4.0.0/siteMods/pcre/8.44.0+0/include
-I/epics/base-7.0.6.1/require/4.0.0/siteMods/calc/3.7.4+1/include

These variables are used by the build system to locate the necessary header files. To see this, let us clear the definition of ASYN_DEP_VERSION and re-run the build.

[iocuser@host:e3-stream]$ echo "ASYN_DEP_VERSION:=" > configure/CONFIG_MODULE.local
[iocuser@host:e3-stream]$ make clean
[iocuser@host:e3-stream]$ make build

# --- snip snip ---

/usr/bin/g++  -D_GNU_SOURCE -D_DEFAULT_SOURCE         -DUSE_TYPED_RSET            -D_X86_64_  -DUNIX  -Dlinux             -MD   -O3 -g   -Wall         -std=c++11     -std=c++11  -mtune=generic               -m64 -fPIC          -I. -I../src/ -I.././src/                   -I/epics/base-7.0.6.1/require/4.0.0/siteMods/pcre/8.44.0+0/include            -I/epics/base-7.0.6.1/require/4.0.0/siteMods/calc/3.7.4+0/include                  -I/epics/base-7.0.6.1/require/4.0.0/siteMods/pcre/8.44.0+0/include            -I/epics/base-7.0.6.1/require/4.0.0/siteMods/calc/3.7.4+0/include -I/epics/base-7.0.6.1/include  -I/epics/base-7.0.6.1/include/compiler/gcc -I/epics/base-7.0.6.1/include/os/Linux                           -c  ../src/AsynDriverInterface.cc
../src/AsynDriverInterface.cc:41:24: fatal error: asynDriver.h: No such file or directory
 #include "asynDriver.h"
                        ^
compilation terminated.

If you look at this output, you will find that -I/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include now is missing. At this point, the build system cannot find asynDriver.h (which is used in src/AsynDriverInterface.cc). If you revert your changes and rebuild, then everything should work correctly.

[iocuser@host:e3-stream]$ rm configure/CONFIG_MODULE.local
[iocuser@host:e3-stream]$ make clean
[iocuser@host:e3-stream]$ make build

Exercise

What is the purpose of creating the CONFIG_MODULE.local file? Why do we modify ASYN_DEP_VERSION there instead of just modifying CONFIG_MODULE?

Updating a dependency

Suppose that for some reason (perhaps due critical bugs in an IOC or some other reason) we need to downgrade from StreamDevice 2.8.22 to 2.8.20. When updating a dependency, we also have to consider all of its dependencies (asyn, pcre, and calc in this case), although we will focus only on asyn.

We first need to check that StreamDevice is compatible with the current version of asyn. It is often useful to check release notes in this case, but it is not a given that every possible combination has been tried, and this may require some testing on your part.

If the older version of StreamDevice and the old version of asyn are compatible, then all you need to do is what was done in Chapter 3.

Note

This assumes that the list of source files for StreamDevice have not changed. If they have, you will have to modify StreamDevice.Makefile to account for any new or removed source files.

If the old version of StreamDevice is not compatible with asyn, then you will need to install a different version of asyn in the current e3 environment. The procedure for that is also the same as in Chapter 3. We can start by checking the current version and seeing what is installed, and then installing the necessary version (4.41.0)

[iocuser@host:e3-asyn]$ make vars # Check the current version
[iocuser@host:e3-asyn]$ make existent
/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn
`-- 4.42.0+0
    |-- asyn_meta.yaml
    |-- db
    |-- dbd
    |-- include
    `-- lib
[iocuser@host:e3-asyn]$ echo "EPICS_MODULE_TAG:=tags/R4-41" > configure/CONFIG_MODULE.local
[iocuser@host:e3-asyn]$ echo "E3_MODULE_VERSION:=4.41.0" >> configure/CONFIG_MODULE.local
[iocuser@host:e3-asyn]$ make vars
[iocuser@host:e3-asyn]$ make init patch build install # You can do these all at once
[iocuser@host:e3-asyn]$ make existent
/epics/base-7.0.6.1/require/4.0.0/siteMods/asyn
|-- 4.41.0+0
|   |-- asyn_meta.yaml
|   |-- db
|   |-- dbd
|   |-- include
|   `-- lib
`-- 4.42.0+0
    |-- asyn_meta.yaml
    |-- db
    |-- dbd
    |-- include
    `-- lib

Note

Between asyn 4-41 and 4-42 there actually are some source and .dbd files that have been added; you may need to remove the following lines

SOURCES += drvPrologixGPIB.c
DBDS += drvPrologixGPIB.dbd

from asyn.Makefile.

This will now allow us to update StreamDevice’s dependencies and install it properly. Save the following as CONFIG_MODULE.local in e3-stream/configure/:

EPICS_MODULE_TAG:=2.8.22
E3_MODULE_VERSION:=2.8.23+0
ASYN_DEP_VERSION:=4.41.0

and then run

[iocuser@host:e3-stream]$ make vars # Again, a good sanity check
[iocuser@host:e3-stream]$ make init patch build install
[iocuser@host:e3-stream]$ make existent
/epics/base-7.0.6.1/require/4.0.0/siteMods/stream
`-- 2.8.22+0
    |-- dbd
    |-- include
    |-- lib
    |-- SetSerialPort.iocsh
    `-- stream_meta.yaml
`-- 2.8.23+0
    |-- dbd
    |-- include
    |-- lib
    |-- SetSerialPort.iocsh
    `-- stream_meta.yaml

Once you have installed a module, it is always a good idea to test that it can be loaded; this is a minimal test that a module must pass! You can do this by running any of the following:

[iocuser@host:~]$ iocsh -r stream,2.8.23
[iocuser@host:~]$ iocsh -r stream,2.8.22
[iocuser@host:~]$ iocsh -r stream

Exercises

  • Which version does the last one load, and why?

  • Which version of asyn is loaded in each case?

  • What happens if you run either of the following?

    [iocuser@host:~]$ iocsh -r stream -r asyn
    [iocuser@host:~]$ iocsh -r asyn -r stream
    

    Can you explain the result?

  • Where is the dependency information stored in the installed module?

Dependency resolution limitations

require is not perfect when it comes to dependency resolution. Consider the following file:

[iocuser@host:~]$ cat /epics/base-7.0.6.1/require/4.0.0/siteMods/stream/2.8.22+0/lib/linux-x86_64/stream.dep
# Generated file. Do not edit.
asyn 4.42.0+0
calc 3.7.4+1
pcre 8.44.0+0

This file is generated at build time, and lists the modules and versions that StreamDevice depends on. require will attempt to load these modules (and versions) when require stream occurs during IOC startup. In that sense, require does not so much resolve dependencies as it does load them.

If there are any conflicts in this process, then require will shut the IOC down. This is what happens when you run iocsh -r asyn -r stream above: since no version is specified for asyn, the latest version (4.42.0) is loaded. Then when StreamDevice is loaded, it depends on asyn 4.42.0. The loaded version of asyn and the requested version of asyn differ, so require shuts the IOC down.

This is a sneak-preview of so-called dependency hell; in this case, the solution is simple. Since asyn is only really being loaded due to StreamDevice’s dependency, the solution is to only load StreamDevice directly and let it take care of loading the correct version of asyn. However, what happens if you need to use StreamDevice, which depends on asyn 4.42.0, and a version of modbus which depends on a different version of asyn?

In general the best practice when writing a startup script for an IOC is only to require the top-level modules. So instead of

require asyn
require sequencer
require sscan
require calc
require stream

it is much simpler to have

require stream

The resulting startup script will be much more maintainable over time and less prone to errors when new module versions are installed.

To clean up the previous changes to your EPICs environment, you can run:

[iocuser@host:e3-stream]$ make uninstall

Exercise

Suppose that your IOC starts fine today with the startup script

require asyn
require stream

What simple and reasonable action could you take that would cause this IOC to fail on startup without changing this script? Why is this a problem from the perspective of the maintainer of a shared e3 environment?

Whence cometh the dependencies

We have so far only discussed the build-time dependencies. These are dependencies that arise when compiling a module, indicated by the inclusion of a C/C++ header file: consider the file AsynDriverInterface.cc from StreamDevice

// --- snip --

#include "asynDriver.h"
#include "asynOctet.h"
#include "asynInt32.h"
#include "asynUInt32Digital.h"
#include "asynGpibDriver.h"

// --- snip ---

When this file is compiled, it generates a .d file that describes the dependencies recognized by make:

[iocuser@host:e3-stream]$ cat StreamDevice/O.7.0.6.1_linux-x86_64/AsynDriverInterface.d
AsynDriverInterface.o: ../src/AsynDriverInterface.cc \
# --- snip ---
 /epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include/asynAPI.h \
 /epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include/asynOctet.h \
 /epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include/asynInt32.h \
 /epics/base-7.0.6.1/include/epicsTypes.h \
 /epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include/asynUInt32Digital.h \
 /epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include/asynGpibDriver.h \
 /epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include/asynDriver.h \
 /epics/base-7.0.6.1/require/4.0.0/siteMods/asyn/4.42.0+0/include/asynInt32.h \

require uses this file to detect build-time dependencies.

Exercise

Similar to asyn, there are header files from calc and pcre that are included which result in them being build-time dependencies. Determine which files include header files from both calc and pcre.

Assignment

There is another kind of dependency, the run-time dependency. This is often associated with dependencies such as StreamDevice where the dependency is based on run-time functionality as connoted by the existence of .proto (protocol) files. For example, consider the e3 module for communicating with a FuG power supply: if you look at the underlying module, you can see that it includes such a protocol file, fug.proto.

  1. Clone the e3 wrapper above, and build and install it.

  2. Look through temporary build files. Can you see where stream is referenced there?

  3. How did e3-fug know that StreamDevice is a dependency? Hint: The configuration files are only part of the story.