The primary goal of an eCos build is to produce the library libtarget.a. A typical eCos build will also generate a number of other targets: extras.o, startup code vectors.o, and a linker script. Some packages may cause additional libraries or targets to be generated. The basic build process involves a number of different phases with corresponding priorities. There are a number of predefined priorities:
Priority | Action |
---|---|
0 | Export header files |
100 | Process compile properties |
and most make_object custom build steps | |
200 | Generate libraries |
300 | Process make custom build steps |
Generation of the extras.o file, the startup code and the linker script actually happens via make custom build steps, typically defined in appropriate HAL packages. The component framework has no special knowledge of these targets.
By default custom build steps for a make_object property happen during the same phase as most compilations, but this can be changed using a -priority option. Similarly custom build steps for a make property happen at the end of a build, but this can also be changed with a -priority option. For example a priority of 50 can be used to run a custom build step between the header file export phase and the main compilation phase. Custom build steps are discussed in more detail below.
Some build systems may run several commands of the same priority in parallel. For example files listed in compile properties may get compiled in parallel, concurrently with make_object custom build steps with default priorities. Since most of the time for an eCos build involves processing compile properties, this allows builds to be speeded up on suitable host hardware. All build steps for a given phase will complete before the next phase is started.
Some build systems may involve a phase before the header files get exported, to update the build and install trees automatically when there has been a change to the configuration savefile ecos.ecc. This is useful mainly for application developers using the command line tools: it would allow users to create the build tree only once, and after any subsequent configuration changes the tree would be updated automatically by the build system. The facility would be analogous to the --enable-maintainer-mode option provide by the autoconf and automake programs. At present no eCos build system implements this functionality, but it is likely to be added in a future release.
The first compulsory phase involves making sure that there is an up to date set of header files in the install tree. Each package can contain some number of header files defining the exported interface. Applications should only use exported functionality. A package can also contain some number of private header files which are only of interest to the implementation, and which should not be visible to application code. The various packages that go into a particular configuration can be spread all over the component repository. In theory it might be possible to make all the exported header files accessible by having a lengthy -I header file search path, but this would be inconvenient both for building eCos and for building applications. Instead all the relevant header files are copied to a single location, the include subdirectory of the install tree. The process involves the following:
The install tree, for example /usr/local/ecos/install, and its include subdirectory /usr/local/ecos/install/include will typically be created when the build tree is generated or updated. At the same time configuration header files will be written to the pkgconf subdirectory, for example /usr/local/ecos/include/pkgconf, so that the configuration data is visible to all the packages and to application code that may wish to examine some of the configuration options.
Each package in the configuration is examined for exported header files. The exact order in which the packages are processed is not defined, but should not matter.
If the package has an include_files property then this lists all the exported header files:
cdl_package <some_package> { … include_files header1.h header2.h } |
If no arguments are given then the package does not export any header files.
cdl_package <some_package> { … include_files } |
The listed files may be in an include subdirectory within the package's hierarchy, or they may be relative to the package's toplevel directory. The include_files property is intended mainly for very simple packages. It can also be useful when converting existing code to an eCos package, to avoid rearranging the sources.
If there is no include_files property then the component framework will look for an include subdirectory in the package, as per the layout conventions. All files, including those in subdirectories, will be treated as exported header files. For example, the math library package contains files include/math.h and include/sys/ieeefp.h, both of which will be exported to the install tree.
As a last resort, if there is neither an include_files property nor an include subdirectory, the component framework will search the package's toplevel directory and all of its subdirectories for files with one of the following suffixes: .h, .hxx, .inl or .inc. All such files will be interpreted as exported header files.
This last resort rule could cause confusion for packages which have no exported header files but which do contain one or more private header files. For example a typical device driver simply implements an existing interface rather than define a new one, so it does not need to export a header file. However it may still have one or more private header files. Such packages should use an include_files property with no arguments.
If the package has one or more exported header files, the next step is to determine where the files should end up. By default all exported header files will just end up relative to the install tree's include subdirectory. For example the math library's math.h header would end up as /usr/local/ecos/include/math.h, and the sys/ieeefp.h header would end up as /usr/local/ecos/include/sys/ieeefp.h. This behaviour is correct for packages like the C library where the interface is defined by appropriate standards. For other packages this behaviour can lead to file name clashes, and the include_dir property should be used to avoid this:
cdl_package CYGPKG_KERNEL { include_dir cyg/kernel } |
This means that the kernel's exported header file include/kapi.h should be copied to /usr/local/ecos/include/cyg/kernel/kapi.h, where it is very unlikely to clash with a header file from some other package.
For typical application developers there will be little or no need for the installed header files to change after the first build. Changes will be necessary only if packages are added to or removed from the configuration. For component writers, the build system should detect changes to the master copy of the header file source code and update the installed copies automatically during the next build. The build system is expected to perform a header file dependency analysis, so any source files affected should get rebuilt as well.
Some build systems may provide additional support for application developers who want to make minor changes to a package, especially for debugging purposes. A header file could be copied from the component repository (which for application developers is assumed to be a read-only resource) into the build tree and edited there. The build system would detect a more recent version of such a header file in the build tree and install it. Care would have to be taken to recover properly if the modified copy in the build tree is subsequently removed, in order to revert to the original behaviour.
When updating the install tree's include subdirectory, the build tree may also perform a clean-up operation. Specifically, it may check for any files which do not correspond to known exported header files and delete them.
Note: At present there is no defined support in the build system for defining custom build steps that generate exported header files. Any attempt to use the existing custom build step support may fall foul of unexpected header files being deleted automatically by the build system. This limitation will be addressed in a future release of the component framework, and may require changing the priority for exporting header files so that a custom build step can happen first.
Once there are up to date copies of all the exported header files in the build tree, the main build can proceed. Most of this involves compiling source files listed in compile properties in the CDL scripts for the various packages, for example:
cdl_package CYGPKG_ERROR { display "Common error code support" compile strerror.cxx … } |
compile properties may appear in the body of a cdl_package, cdl_component, cdl_option or cdl_interface. If the option or other CDL entity is active and enabled, the property takes effect. If the option is inactive or disabled the property is ignored. It is possible for a compile property to list multiple source files, and it is also possible for a given CDL entity to contain multiple compile properties. The following three examples are equivalent:
cdl_option <some_option> { … compile file1.c file2.c file3.c } cdl_option <some_option> { … compile file1.c compile file2.c compile file3.c } cdl_option <some_option> { … compile file1.c file2.c compile file3.c } |
Packages that follow the directory layout conventions should have a subdirectory src, and the component framework will first look for the specified files there. Failing that it will look for the specified files relative to the package's root directory. For example if a package contains a source file strerror.cxx then the following two lines are equivalent:
compile strerror.cxx compile src/strerror.cxx |
In the first case the component framework will find the file immediately in the packages src subdirectory. In the second case the framework will first look for a file src/src/strerror.cxx, and then for str/strerror.cxx relative to the package's root directory. The result is the same.
The file names may be relative paths, allowing the source code to be split over multiple directories. For example if a package contains a file src/sync/mutex.cxx then the corresponding CDL entry would be:
compile sync/mutex.cxx |
All the source files relevant to the current configuration will be identified when the build tree is generated or updated, and added to the appropriate makefile (or its equivalent for other build systems). The actual build will involve a rule of the form:
<object file> : <source file> $(CC) -c $(INCLUDE_PATH) $(CFLAGS) -o $@ $< |
The component framework has built-in knowledge for processing source files written in C, C++ or assembler. These should have a .c, .cxx and .S suffix respectively. The current implementation has no simple mechanism for extending this with support for other languages or for alternative suffixes, but this should be addressed in a future release.
The compiler command that will be used is something like arm-elf-gcc. This consists of a command prefix, in this case arm-elf, and a specific command such as gcc. The command prefix will depend on the target architecture and is controlled by a configuration option in the appropriate HAL package. It will have a sensible default value for the current architecture, but users can modify this option when necessary. The command prefix cannot be changed on a per-package basis, since it is usually essential that all packages are built with a consistent set of tools.
The $(INCLUDE_PATH) header file search path consists of at least the following:
The include directory in the install tree. This allows source files to access the various header files exported by all the packages in the configuration, and also the configuration header files.
The current package's root directory. This ensures that all files in the package are accessible at build time.
The current package's src subdirectory, if it is present. Generally all files to be compiled are located in or below this directory. Typically this is used to access private header files containing implementation details only.
The compiler flags $(CFLAGS) are determined in two steps. First the appropriate HAL package will provide a configuration option defining the global flags. Typically this includes flags that are needed for the target processor, for example -mcpu=arm9, various flags related to warnings, debugging and optimization, and flags such as -finit-priority which are needed by eCos itself. Users can modify the global flags option as required. In addition it is possible for existing flags to be removed from and new flags to be added to the current set on a per-package basis, again by means of user-modifiable configuration options. More details are given below.
Component writers can assume that the build system will perform full header file dependency analysis, including dependencies on configuration headers, but the exact means by which this happens is implementation-defined. Typical application developers are unlikely to modify exported or private header files, but configuration headers are likely to change as the configuration is changed to better meet the needs of the application. Full header file dependency analysis also makes things easier for the component writers themselves.
The current directory used during a compilation is an implementation detail of the build system. However it can be assumed that each package will have its own directory somewhere in the build tree, to prevent file name clashes, that this will be the current directory, and that intermediate object files will end up here.
Once all the compile and make_object properties have been processed and the required object files have been built or rebuilt, these can be collected together in one or more libraries. The archiver will be the ar command corresponding to the current architecture, for example powerpc-eabi-ar. By default al of the object files will end up in a single library libtarget.a. This can be changed on a per-package basis using the library property in the body of the corresponding cdl_package command, for example:
cdl_package <SOME_PACKAGE> { … library libSomePackage.a } |
However using different libraries for each package should be avoided. It makes things more difficult for application developers since they now have to link the application code with more libraries, and possibly even change this set of libraries when packages are added to or removed from the configuration. The use of a single library libtarget.a avoids any complications.
It is also possible to change the target library for individual files, using a -library option with the corresponding compile or make_object property. For example:
compile -library=libSomePackage.a hello.c make_object -library=libSomePackage.a { … } |
Again this should be avoided because it makes application development more difficult. There is one special library which can be used freely, libextras.a, which is used to generate the extras.o file as described below.
The order in which object files end up in a library is not defined. Typically each library will be created directly in the install tree, since there is little point in generating a file in the build tree and then immediately copying it to the install tree.
Package sources files normally get compiled and then added to a library, by default libtarget.a, which is then linked with the application code. Because of the usual rules for linking with libraries, augmented by the use of link-time garbage collection, this means that code will only end up in the final executable if there is a direct or indirect reference to it in the application. Usually this is the desired behaviour: if the application does not make any use of say kernel message boxes, directly or indirectly, then that code should not end up in the final executable taking up valuable memory space.
In a few cases it is desirable for package code to end up in the final executable even if there are no direct or indirect references. For example, device driver functions are often not called directly. Instead the application will access the device via the string "/dev/xyzzy" and call the device functions indirectly. This will be impossible if the functions have been removed at link-time.
Another example involves static C++ objects. It is possible to have a static C++ object, preferably with a suitable constructor priority, where all of the interesting work happens as a side effect of running the constructor. For example a package might include a monitoring thread or a garbage collection thread created from inside such a constructor. Without a reference by the application to the static object the latter will never get linked in, and the package will not function as expected.
A third example would be copyright messages. A package vendor may want to insist that all products shipped using that package include a particular message in memory, even though many users of that package will object to such a restriction.
To meet requirements such as these the build system provides support for a file extras.o, which always gets linked with the application code via the linker script. Because it is an object file rather than a library everything in the file will be linked in. The extras.o file is generated at the end of a build from a library libextras.a, so packages can put functions and variables in suitable source files and add them to that library explicitly:
compile -library=libextras.a xyzzy.c compile xyzzy_support.c |
In this example xyzzy.o will end up in libextras.a, and hence in extras.o and in the final executable. xyzzy_support.o will end up in libtarget.a as usual, and is subject to linker garbage collection.
Caution |
Some of the details of compiler selection and compiler flags described below are subject to change in future revisions of the component framework, although every reasonable attempt will be made to avoid breaking backwards compatibility. |
The build system needs to know what compiler to use, what compiler flags should be used for different stages of the build and so on. Much of this information will vary from target to target, although users should be able to override this when appropriate. There may also be a need for some packages to modify the compiler flags. All platform HAL packages should define a number of options with well-known names, along the following lines (any existing platform HAL package can be consulted for a complete example):
cdl_component CYGBLD_GLOBAL_OPTIONS { flavor none parent CYGPKG_NONE … cdl_option CYGBLD_GLOBAL_COMMAND_PREFIX { flavor data default_value { "arm-elf" } … } cdl_option CYGBLD_GLOBAL_CFLAGS { flavor data default_value "-Wall -g -O2 …" … } cdl_option CYGBLD_GLOBAL_LDFLAGS { flavor data default_value "-g -nostdlib -Wl,--gc-sections …" … } } |
The CYGBLD_GLOBAL_OPTIONS
component serves to
collect together all global build-related options. It has the flavor
none since disabling all of these options would
make it impossible to build anything and hence is not useful. It is
parented immediately below the root of the configuration hierarchy,
thus making sure that it is readily accessible in the graphical
configuration tool and, for command line users, in the
ecos.ecc save file.
Note: Currently the parent property lists a parent of
CYGPKG_NONE
, rather than an empty string. This could be unfortunate if there was ever a package with that name. The issue will be addressed in a future release of the component framework.
The option CYGBLD_GLOBAL_COMMAND_PREFIX
defines
which tools should be used for the current target. Typically this is
determined by the processor on the target hardware. In some cases a
given target board may be able to support several different
processors, in which case the default_value expression could select
a different toolchain depending on some other option that is used to
control which particular processor.
CYGBLD_GLOBAL_COMMAND_PREFIX
is modifiable rather
than calculated, so users can override this when necessary.
Given a command prefix such as arm-elf, all C source files will be compiled with arm-elf-gcc, all C++ sources will be built using arm-elf-g++, and arm-elf-ar will be used to generate the library. This is in accordance with the usual naming conventions for GNU cross-compilers and similar tools. For the purposes of custom build steps, tokens such as $(CC) will be set to arm-elf-gcc.
The next option, CYGBLD_GLOBAL_CFLAGS
, is used to
provide the initial value of $(CFLAGS). Some
compiler flags such as -Wall and
-g are likely to be used on all targets. Other
flags such as -mcpu=arm7tdmi will be
target-specific. Again this is a modifiable option, so the user can
switch from say -O2 to -Os if
desired. The option CYGBLD_GLOBAL_LDFLAGS
serves
the same purpose for $(LDFLAGS) and linking. It is
used primarily when building test cases or possibly for some custom
build steps, since building eCos itself generally involves building
one or more libraries rather than executables.
Some packages may wish to add certain flags to the global set, or possibly remove some flags. This can be achieved by having appropriately named options in the package, for example:
cdl_component CYGPKG_KERNEL_OPTIONS { display "Kernel build options" flavor none … cdl_option CYGPKG_KERNEL_CFLAGS_ADD { display "Additional compiler flags" flavor data default_value { "" } … } cdl_option CYGPKG_KERNEL_CFLAGS_REMOVE { display "Suppressed compiler flags" flavor data default_value { "" } … } cdl_option CYGPKG_KERNEL_LDFLAGS_ADD { display "Additional linker flags" flavor data default_value { "" } … } cdl_option CYGPKG_KERNEL_LDFLAGS_REMOVE { display "Suppressed linker flags" flavor data default_value { "" } … } } |
In this example the kernel does not modify the global compiler flags by default, but it is possible for the users to modify the options if desired. The value of $(CFLAGS) that is used for the compilations and custom build steps in a given package is determined as follows:
Start with the global settings from
CYGBLD_GLOBAL_CFLAGS
, for example
-g -O2.
Remove any flags specified in the per-package CFLAGS_REMOVE option, if any. For example if -O2 should be removed for this package then $(CFLAGS) would now have a value of just -g.
Then concatenate the flags specified by the per-package CFLAGS_ADD option, if any. For example if -Os should be added for the current package then the final value of $(CFLAGS) will be -g -Os.
$(LDFLAGS) is determined in much the same way.
Note: The way compiler flags are handled at present has numerous limitations that need to be addressed in a future release, although it should suffice for nearly all cases. For the time being custom build steps and in particular the make_object property can be used to work around the limitations.
Amongst the issues, there is a specific problem with package encapsulation. For example the math library imposes some stringent requirements on the compiler in order to guarantee exact IEEE behavior, and may need special flags on a per-architecture basis. One way of handling this is to have
CYGPKG_LIBM_CFLAGS_ADD
andCYGPKG_LIBM_CFLAGS_REMOVE
default_value expressions which depend on the target architecture, but such expressions may have to updated for each new architecture. An alternative approach would allow the architectural HAL package to modify the default_value expressions for the math library, but this breaks encapsulation. A third approach would allow some architectural HAL packages to define one or more special options with well-known names, and the math library could check if these options were defined and adjust the default values appropriately. Other packages with floating point requirements could do the same. This approach also has scalability issues, in particular how many such categories of options would be needed? It is not yet clear how best to resolve such issues.
Note: When generating a build tree it would be desirable for the component framework to output details of the tools and compiler flags in a format that can be re-used for application builds, for example a makefile fragment. This would make it easier for application developers to use the same set of flags as were used for building eCos itself, thus avoiding some potential problems with incompatible compiler flags.
Caution |
Some of the details of custom build steps as described below are subject to change in future revisions of the component framework, although every reasonable attempt will be made to avoid breaking backwards compatibility. |
For most packages simply listing one or more source files in a compile property is sufficient. These files will get built using the appropriate compiler and compiler flags and added to a library, which then gets linked with application code. A package that can be built in this way is likely to be more portable to different targets and build environments, since it avoids build-time dependencies. However some packages have special needs, and the component framework supports custom build steps to allow for these needs. There are two properties related to this, make and make_object, and both take the following form:
make { <target_filepath> : <dependency_filepath> … <command> ... } |
Although this may look like makefile syntax, and although some build environments will indeed involve generating makefiles and running make, this is not guaranteed. It is possible for the component framework to be integrated with some other build system, and custom build steps should be written with that possibility in mind. Each custom build step involves a target, some number of dependency files, and some number of commands. If the target is not up to date with respect to one or more of the dependencies then the commands need to be executed.
Only one target can be specified. For a make_object property this target must be an object file. For a make property it can be any file. In both cases it must refer to a physical file, the use of phony targets is not supported. The target should not be an absolute path name. If the generated file needs to end up in the install tree then this can be achieved using a <PREFIX> token, for example:
make { <PREFIX>/lib/mytarget : … ... } |
When the build tree is generated and the custom build step is added to the makefile (or whatever build system is used) <PREFIX> will be replaced with the absolute path to the install tree.
All the dependencies must also refer to physical files, not to phony targets. These files may be in the source tree. The <PACKAGE> token can be used to indicate this: when the build tree is generated this token will be replaced with the absolute path to the package's root directory in the component repository, for example:
make_object { xyzzy.o : <PACKAGE>/src/xyzzy.c … |
If the component repository was installed in /usr/local/ecos and this custom build step existed in version 1_5 of the kernel, <PACKAGE> would be replaced with /usr/local/ecos/packages/kernel/v1_5.
Alternatively the dependencies may refer to files that are generated during the build. These may be object files resulting from compile properties or other make_object properties, or they may be other files resulting from a make property, for example:
compile plugh.c make_object { xyzzy.o : plugh.o … } |
No other token or makefile variables may be used in the target or dependency file names. Also conditionals such as ifneq and similar makefile functionality must not be used.
Similarly the list of commands must not use any makefile conditionals or similar functionality. A number of tokens can be used to provide access to target-specific or environmental data. Note that these tokens look like makefile variables, unlike the <PREFIX> and <PACKAGE> tokens mentioned earlier:
Token | Purpose | Example value |
---|---|---|
$(AR) | the GNU archiver | mips-tx39-elf-ar |
$(CC) | the GNU compiler | sh-elf-gcc |
$(CFLAGS) | compiler flags | -O2 -Wall |
$(COMMAND_PREFIX) | the triplet prefix | mn10300-elf- |
$(INCLUDE_PATH> | header file search path | -I. -Isrc/misc |
$(LDFLAGS) | linker flags | -nostdlib -Wl,-static |
$(OBJCOPY) | the objcopy utility | arm-elf-objcopy |
$(PREFIX) | location of the install tree | /home/fred/ecos-install |
$(REPOSITORY) | location of the component repository | /home/fred/ecos/packages |
In addition commands in a custom build step may refer to the target and the dependencies using $@, $<, $^ and $*, all of which behave as per GNU make syntax. The commands will execute in a suitable directory in the build tree.
The current directory used during a custom build step is an implementation detail of the build system. However it can be assumed that each package will have its own directory somewhere in the build tree, to prevent file name clashes, and that this will be the current directory. In addition any object files generated as a result of compile properties will be located here as well, which is useful for custom build steps that depend on a .o file previously generated.
Any temporary files created by a custom build step should be generated in the build tree (in or under the current directory). Such files should be given a .tmp file extension to ensure that they are deleted during a make clean or equivalent operation.
If a package contains multiple custom build steps with the same priority, it is possible that these build steps will be run concurrently. Therefore these custom build steps must not accidentally use the same file names for intermediate files.
Care has to be taken to make sure that the commands in a custom build step will run on all host platforms, including Windows NT as well as Linux and other Unix systems. For example, all file paths should use forward slashes as the directory separator. It can be assumed that Windows users will have a full set of CygWin tools installed and available on the path. The GNU coding standards provide some useful guidelines for writing portable build rules.
A custom build step must not make any assumptions concerning the version of another package. This enforces package encapsulation, preventing one package from accessing the internals of another.
No assumptions should be made about the target platform, unless the package is inherently specific to that platform. Even then it is better to use the various tokens whenever possible, rather than hard-coding in details such as the compiler. For example, given a custom build step such as:
arm-elf-gcc -c -mcpu=arm7di -o $@ $< |
Even if this build step will only be invoked on ARM targets, it could cause problems. For example the toolchain may have been installed using a prefix other than arm-elf. Also, if the user changes the compiler flags then this would not be reflected in the build step. The correct way to write this rule would be:
$(CC) -c $(CFLAGS) -o $@ $< |
Some commands such as the compiler, the archiver, and objcopy are required sufficiently often to warrant their own tokens, for example $(CC) and $(OBJCOPY). Other target-specific commands are needed only rarely and the $(COMMAND_PREFIX) token can be used to construct the appropriate command name, for example:
$(COMMAND_PREFIX)size $< > $@ |
Custom build steps should not be used to build host-side executables, even if those executables are needed to build parts of the target side code. Support for building host-side executables will be added in a future version of the component framework, although it will not necessarily involve these custom build steps.
By default custom build steps defined in a make_object property have a priority of 100, which means that they will be executed in the same phase as compilations resulting from a compile property. It is possible to change the priority using a property option, for example:
make_object -priority 50 { … } |
Specifying a priority smaller than a 100 means that the custom build step happens before the normal compilations. Priorities between 100 and 200 happen after normal compilations but before the libraries are archived together. make_object properties should not specify a priority of 200 or later.
Custom build steps defined in a make property have a default priority of 300, and so they will happen after the libraries have been built. Again this can be changed using a -priority property option.
Linking an application requires the application code, a linker script, the eCos library or libraries, the extras.o file, and some startup code. Depending on the target hardware and how the application gets booted, this startup code may do little more than branching to main(), or it may have to perform a considerable amount of hardware initialization. The startup code generally lives in a file vectors.o which is created by a custom build step in a HAL package. As far as application developers are concered the existence of this file is largely transparent, since the linker script ensures that the file is part of the final executable.
This startup code is not generally of interest to component writers, only to HAL developers who are referred to one of the existing HAL packages for specific details. Other packages are not expected to modify the startup in any way. If a package needs some work performed early on during system initialization, before the application's main entry point gets invoked, this can be achieved using a static object with a suitable constructor priority.
Note: It is possible that the extras.o support, in conjunction with appropriate linker script directives, could be used to eliminate the need for a special startup file. The details are not yet clear.
Caution |
This section is not finished, and the details are subject to change in a future release. Arguably linker script issues should be documented in the HAL documentation rather than in this guide. |
Generating the linker script is the responsibility of the various HAL packages that are applicable to a given target. Developers of components other than HAL packages need not be concerned about what is involved. Developers of new HAL packages should use an existing HAL as a template.
Note: It may be desirable for some packages to have some control over the linker script, for example to add extra alignment details for a particular section. This can be risky because it can result in subtle portability problems, and the current component framework has no support for any such operations. The issue may be addressed in a future release.