24.5. Cross-Platform Dependency Example

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.