In the doc/example/cross-platform
directory,
there is a build tree that illustrates abuild's ability to
enhance dependency declaration with platform type or platform
information. In this example, we show a platform-independent
code generator that calls a C++ program to do some of its work.
We also show a program that uses this code generator. We'll
examine these build items from the bottom up in the dependency
chain. Our first several items are quite straightforward and are
no different in how they work from what we've seen before.
First, look at lib
:
cross-platform/lib/Abuild.conf
name: lib platform-types: native
cross-platform/lib/Abuild.mk
TARGETS_lib := lib SRCS_lib_lib := lib.cc RULES := ccxx
cross-platform/lib/Abuild.interface
LIBDIRS = $(ABUILD_OUTPUT_DIR) LIBS = lib INCLUDES = .
This build item defines a function f
that
returns the square of its integer argument. Here is
lib.cc
:
cross-platform/lib/lib.cc
#include "lib.hh" int f(int n) { return n * n; }
Next, look at calculate
:
cross-platform/calculate/Abuild.conf
name: calculate platform-types: native deps: lib
cross-platform/calculate/Abuild.mk
TARGETS_bin := calculate SRCS_bin_calculate := calculate.cc RULES := ccxx
cross-platform/calculate/calculate.cc
#include <lib.hh> #include <iostream> #include <stdlib.h> int main(int argc, char* argv[]) { for (int i = 1; i < argc; ++i) { int n = atoi(argv[i]); std::cout << n << "\t" << f(n) << std::endl; } return 0; }
This is a simple program that takes a number of arguments on the
command line and prints tab-delimited output with the number in
column 1 and the square of the number in column 2. It uses the
f
function in lib
to
do the square calculation, and therefore depends on the
lib
build item.
So far, we haven't seen anything particularly unusual in this
example, but this is where it starts to get interesting. The
material here is tricky. To follow this, you need to remember
that variables set in Abuild.interface
files
of build items you depend on are available to you as
make variables. We can use
make's export
command to make those variables available in the environment.
The calculate
build item exports the name
of its program in an interface variable in its
Abuild.interface
file by creating a variable
called CALCULATE
:
cross-platform/calculate/Abuild.interface
declare CALCULATE filename CALCULATE = $(ABUILD_OUTPUT_DIR)/calculate after-build after.interface
As with all interface variables, this will be available as a make
variable within Abuild.mk
. It also includes
the after-build
file
after.interface
:
cross-platform/calculate/after.interface
no-reset CALCULATE reset-all
This file protects the CALCULATE
variable from
being reset, and then calls reset-all
. In
this way, items that depend on calculate
will not automatically inherit the interface from
lib
or any of its dependencies. This
represents the intention that a dependency on the
calculate
build item would be set up if
you wanted to run the
calculate
program rather than to link with
or include header files from the libraries used to build
calculate
. In other words, we treat
calculate
as a black box and don't care
how it was built. This works because the
CALCULATE
variable, which contains the name of
the calculate
program, was protected from
reset, but the LIBS
,
LIBDIRS
, and INCLUDES
variables have been cleared. In that way, a user of the
calculate
build item won't link against
the lib
library or be able to include the
lib.hh
header file unless they had also
declared a dependency on lib
. If we
hadn't cleared these variables, any code that depended on the
calculate
build item may well still have
worked, but it would have had some excess libraries, include
files, and library directories added to its compilation commands.
In some cases, this could create unanticipated code dependencies,
expose you to namespace collisions, or cause unwanted static
initializers to be run.
Next, look at the codegen
build item.
This build item runs a code generator,
gen_code.pl
, which in turn runs the
calculate
program. We provide the name of
our code generator in the Abuild.interface
file:
cross-platform/codegen/Abuild.interface
declare CODEGEN filename CODEGEN = gen_code.pl
This build item provides a rules implementation file in
rules/object-code/codegen.mk
(and a help
file in rules/object-code/codegen-help.txt
)
for creating a file called generate.cc
. It
calls the gen_code.pl
program, which it
finds using the CODEGEN
interface variable, to
do its job. The gen_code.pl
program uses
the CALCULATE
environment variable to find the
actual calculate program. Although we have the
CALCULATE
variable as a make variable
(initialized from calculate
's
Abuild.interface
file), we need to export it
so that it will become available in the environment. We also
pass the file named in the NUMBERS
variable to
the code generator. Here are the
codegen-help.txt
file, the
codegen.mk
file, and the code generator:
cross-platform/codegen/rules/object-code/codegen.mk
# Export this variable to the environment so we can access it from # $(CODEGEN) using the CALCULATE environment variable. We could also # have passed it on the command line. export CALCULATE generate.cc: $(NUMBERS) $(CODEGEN) perl $(CODEGEN) $(SRCDIR)/$(NUMBERS) > $@
cross-platform/codegen/rules/object-code/codegen-help.txt
Set NUMBERS to the name of a file that contains a list of numbers, one per line, to pass to the generator. The file generate.cc will be generated.
cross-platform/codegen/gen_code.pl
require 5.008; use warnings; use strict; use File::Basename; my $whoami = basename($0); my $calculate = $ENV{'CALCULATE'} or die "$whoami: CALCULATE is not defined\n"; my $file = shift(@ARGV); open(F, "<$file") or die "$whoami: can't open $file: $!\n"; my @numbers = (); while (<F>) { s/\r?\n//; if (! m/^\d+$/) { die "$whoami: each line of $file must be a number\n"; } push(@numbers, $_); } print <<EOF \#include <iostream> void generate() { EOF ; open(P, "$calculate " . join(' ', @numbers) . "|") or die "$whoami: can't run calculate\n"; while (<P>) { if (m/^(\d+)\t(\d+)/) { print " std::cout << $1 << \" squared is \" << $2 << std::endl;\n"; } } print <<EOF } EOF ;
In order for this to work, the codegen
build item must depend on the calculate
build item. Ordinarily, abuild will not allow this since the
calculate
build item would not be able to
be built on the indep platform, which is the only
platform on which codegen
is built. To
get around this, codegen
's
Abuild.conf
specifies a
-platform
argument to its declaration of its
dependency on calculate
:
cross-platform/codegen/Abuild.conf
name: codegen platform-types: indep deps: calculate -platform=native:option=release
The argument -platform=native:option=release
tells abuild to make codegen
depend on
the instance of calculate
built on the
first native platform that has the
release option, if any; otherwise, it depends on the
highest priority native platform. Note that this
will cause the release option of the appropriate
platform to be built for calculate
and its
dependencies even if they would not have otherwise been built.
This is an example of abuild's ability to build on additional
platforms on an as-needed basis. For details on exactly how
abuild resolves such dependencies, see Section 33.6, “Construction of the Build Graph”.
Notice that this code generator uses an interface variable, in
this case $(CALCULATE)
, to refer to a file in
the calculate
build item. Not only is
this a best practice since it avoids having us have to know the
location of a file in another build item, but it is actually the
only way we can find the calculate
program:
abuild doesn't provide any way for us to know the name of the
output directory from the calculate
build
item we are using except through the interface system. (The
value of the ABUILD_OUTPUT_DIR
variable would
be the output directory for the item currently being built, not
the output directory that we want from the
calculate
build item.) We also use an
interface variable to refer to the code generator within our own
build item, though in this case, it would not be harmful to use
$(abDIR_codegen)/gen_code.pl
instead.
[52]
Finally, look at the prog
build item.
This build item depends on the codegen
build item. Its Abuild.mk
defines the
NUMBERS
variable as required by
codegen
, which it lists in its
RULES
variable. This build item doesn't know
or care about the interface of the lib
build item, which has been hidden from it by the
reset-all
in
calculate
's
after.interface
. (If it wanted to, it could
certainly also depend on lib
, in which
case it would get lib
's interface.) In
fact, running abuild ccxx_debug will show that
prog
's INCLUDES
,
LIBS
, and LIBDIRS
variables
are all empty:
cross-platform-ccxx_debug.out
abuild: build starting abuild: prog (abuild-<native>): ccxx_debug make: Entering directory `--topdir--/cross-platform/prog/abuild-<native>' INCLUDES = LIBDIRS = LIBS = make: Leaving directory `--topdir--/cross-platform/prog/abuild-<native>' abuild: build complete
[52]
Actually, there is something a bit more subtle going on here.
If we didn't have an Abuild.interface
file
or an Abuild.mk
file, abuild would not
allow this build item to declare a platform type, and it would
automatically inherit its platform type from its dependency or
become a special build item of platform type all,
as discussed in Section 33.6, “Construction of the Build Graph”. In that case,
abuild would not allow us to declare a platform-specific
dependency, and although the code generator would still work
just fine, this wouldn't be much of an example! The construct
illustrated here is still useful though as this is exactly how
it would have to work if there were other values to be exported
through Abuild.interface
or any products
that needed to be built by this build item itself. For
example, if the code generator example had been written in Java
instead of perl, this pattern would have been the only way to
achieve the goal.