Table of Contents
The abuild interface system is the mechanism through which
abuild provides encapsulation. Its purpose is to allow build
items to provide information about the products they provide to
other build items. Build items provide their interfaces with the
Abuild.interface
file. This chapter
describes the interface system and provides details about the
syntax and semantics of Abuild.interface
and
other abuild interface files.
This section contains a prose description of the interface
system's functionality and presents the basic syntax of
Abuild.Interface
without providing all of
the details. This material provides the basis for understanding
how the interface functionality works. In the next section, we
go over the details.
The Abuild.interface
file has a fairly
simple syntax that supports variable declarations, variable
assignments, and conditionals. Interface files are rigorously
validated. Any errors detected in an interface file are
considered build failures which, as such, will prevent abuild
from attempting to build the item with the incorrect interface
and any items that depend on it. Most
Abuild.interface
files will just set
existing variables to provide specific information about that
item's include and library information, classpath information, or
whatever other standard information may be needed depending upon
the type of item it is. For casual users, a full understanding
of this material is not essential, but for anyone trying to debug
interface issues or create support within abuild for more
complex cases, it will be important to understand how abuild
reads Abuild.interface
files.
The basic purpose of Abuild.interface
is to
set variables that are ultimately used by a build item to access
its dependencies. The basic model is that an item effectively
reads the Abuild.interface
files of all its
dependencies in dependency order. (This is not exactly what
happens. For the full story, see Section 33.7, “Implementation of the Abuild Interface System”.) As each file is read,
it adds information to the lists of include paths, libraries,
library directories, compiler flags, classpath, etc. All
variables referenced by Abuild.interface
are
global variables, even if they are declared inside the body of a
conditional, much as is the case with shell scripts or makefiles.
Although this is not literally what happens, the best way to
think about how abuild reads interface files is to imagine
that, for each build item, all of the interface files for its
dependencies along with its own interface file are concatenated
in dependency order and that the results of that concatenation
are processed from top to bottom, skipping over any blocks inside
of false conditional statements.
Once abuild parses the Abuild.interface
files of all of a build item's dependencies and that of the build
item itself, the names and values of the resulting variables are
passed to the backends by writing them to the abuild
dynamic output file, which is called
.ab-dynamic.mk
for
make-based builds and
.ab-dynamic.groovy
for
Groovy/ant-based builds. The dynamic
output file is created in the output directory. Although users
running abuild don't even have to know this file exists,
peeking at it is a useful way to see the results of parsing all
the Abuild.interface
files in a build item's
dependency chain.
The Abuild.interface
file contains the
following items:
Comments
Variable declarations
Variable assignments
After-build file specifications
Target type restrictions
Conditionals
Similar to make or shell script syntax, each statement is
terminated by the end of the line. Whitespace characters (spaces
or tabs) are used to separate words. A backslash
(\
) as the last character of the line may be
used to continue long statements onto the next line of the file,
in which case the newline is treated as a word delimiter like any
other whitespace.
[29]
Any line that starts with a #
character
optionally preceded by whitespace is ignored entirely. Comment
lines have no effect on line continuation. In other words, if
line one ends with a continuation character and line two is a
comment, line one is continued on line three. This makes it
possible to embed comments in multiline lists of values. In this
example, the value of ODDS
would be
one three
:
ODDS = \ one \ # odd numbers only, please # two \ three
Characters that have special meanings (space, comma, equal, etc.) may be quoted by preceding them by a backslash. For consistency, a backslash followed by any character is treated as that character. This way, the semantics of backslash quoting won't change if additional special characters are added in the future.
All variables must be declared, though most
Abuild.interface
files will be assigning to
variables that have already been declared in other interface
files. There are no variable scoping rules: all variables are
global, even if declared inside a conditional block. Variable
names may contain alphanumeric characters, dash, underscore, and
period. By convention, make-based rules use all uppercase
letters in variable names. This convention also has the
advantage of avoiding potential conflict with reserved
statements. Java-based rules typically use lower-case
period-separated properties. Ultimately abuild interface
variables become make variables or
ant properties and keys in parameter
tables for Groovy, which is the basis
for these conventions. Note, however, that variables of both
naming styles may be used by either backend, and some of
abuild's predefined interface variables that are available to
both make and
Groovy/ant are of the all upper-case
variety.
Once declared, a variable may be assigned to or referenced. A
variable is referenced by enclosing its name with parentheses and
preceding it by a dollar sign (as in
$(VARIABLE)
), much like with standard make
syntax, except that there is no special case for single-character
variable names. Other than using the backslash character to
quote single characters, there is no quoting syntax: the single
and double quote characters are treated as ordinary characters
with no special meanings.
Environment variables may be referenced using the syntax
$(ENV:VARIABLE)
. Unlike many other systems
which treat undefined environment variables as the empty string,
abuild will trigger an error condition if the environment
variable does not exist unless a default value is provided. A
default value can be provided using the syntax
$(ENV:VARIABLE:default-value)
. The
default-value
portion of the string
may not contain spaces, tabs, or parentheses.
[30]
Although it can sometimes be useful to have abuild interface
files initialize interface variables from the environment, this
feature should be used sparingly as it is possible to make a
build become overly dependent on the environment in this way.
(Even without this feature, there are other ways to fall into
this trap that are even worse.) Note that environment variables
are not abuild variables. They are expanded as strings and can
be used in the interface file wherever ordinary strings can be
used.
In addition, starting in version 1.1.1, abuild can access
command-line parameters of the form
VAR=val
from interface files. This
works identically to environment variables. Parameter references
are of the form $(PARAM:PARAMETER)
or
$(PARAM:PARMETER:default-value)
. As with
environment variable references, accessing an unspecified
parameter without a default is an error, and parameter expansions
are treated as strings by the interface parser. This feature
should also be used sparingly as it can create plenty of
opportunity for unpredictable builds. The main valid use case
for accessing parameters from an interface file would be to allow
special debugging changes that allow modifying build behavior
from the command-line for particular circumstances. Keep in mind
that changing parameters on the command line has no impact on
dependencies, so gratuitous and careless use of this feature can
lead to unreproducible builds. That said, this feature does not
make abuild inherently less safe since it has always been
possible to access parameters and the environment directly from
make code.
Variables may contain single scalar values or they may contain
lists of values of one of the three supported types:
boolean
, string
, or
filename
.
Boolean variables are simple true/false values. The values
1
and true
are
interpreted interchangeably as true, and the values
0
and false
are
interpreted interchangeably as false. Regardless of whether the
word or numeric value is used to assign to boolean variables, the
normalized values of 0
and
1
are passed to the backend build system.
(For simplicity and consistency, this is true even for the Groovy
backend, which could handle actual boolean values instead.)
String variables just contain arbitrary text. It is possible to
embed spaces in string variables by quoting them with a
backslash, but keep in mind that not all backends handle spaces
in single-word variable values cleanly. For example, dealing
with embedded spaces in variable names in GNU Make is impractical
since it uses space as a word delimiter and offers no specific
quoting mechanisms. The values of filename variables are
interpreted to be path names. Path names may be specified with
either forward slashes or backslashes on any platform. Relative
paths (those that do not start with a path separator character
or, on Windows, also a drive letter) are interpreted as
relative to the file in which they are
assigned, not the file in which they are referenced as
is the case with make. This means
that build items can export information about their local files
using relative paths without having to use any special variables
that point to their own local directories. Although this is
different from how make works, it is the only sensible semantic
for files that are referenced from multiple locations, and it is
one of the most important and useful features of the abuild
interface system.
List variables may contain multiple space-separated words.
Assignments to list variables may span multiple lines by using a
trailing backslash to indicate continuation to the next line.
Each element of a list must be the same type. Lists can be made
of any of the supported scalar types. (Lists of boolean values
are supported, though they are essentially useless.) List
variables must be declared as either append
or
prepend
, depending upon whether successive
assignments are appended or prepended to the value of the list.
This is described in more depth when we discuss variable
assignment below.
Scalar variables may be assigned to in one of three ways: normal, override, and fallback. A normal assignment to a scalar variable fails if the variable already has a value. An override assignment initializes a previously uninitialized variable and replaces any previously assigned value. A fallback assignment sets the value of the variable only if it has not previously been initialized. Uninitialized variables are passed to the backend as empty strings. It is legal to initialize a string variable to the empty string, and doing this is distinct from not initializing it.
List variables work differently from anything you're likely to
have encountered in other environments, but they offer
functionality that is particularly useful when building software.
List variables may be assigned to multiple times. The value in
each individual assignment may contain zero or more words.
Depending on whether the variable was declared as
append
or prepend
, the
values are appended to or prepended to the list in the order in
which they appear in the specific assignment. An example is
provided below.
Scalar and list variables can both be reset using the
reset
statement. This resets the variable back
to its initial state, which is uninitialized for scalars and
empty for lists.
Any variable assignment statement can be made conditional upon the presence of a given interface flag. Interface flags are introduced in Chapter 23, Interface Flags, and the details of how to use them in interface files are discussed later in this chapter.
Abuild supports nested conditionals, each of which may contain
an if
clause, zero or more
elseif
clauses, and an optional
else
clause. The abuild interface syntax
supports no relational operators: all conditionals are expressed
in terms of function calls, the details of which are provided
below.
In addition to supporting variables and conditionals, it is
possible to specify that certain variables are relevant only to
build items of a specific target type. A target type restriction
applies until the next target-type
directive
or until the end of the current file and all the files it loads
as after-build
files. By default,
declarations in an Abuild.interface
file
apply to all target types. The vast majority of interface files
will not have to include any target type restrictions.
It is possible for a build item to contain interface information
that is intended for items that depend on it but not intended for
the item itself. Typical uses cases would include when some of
this information is a product of the build or when a build item
needs to modify interface information provided by a dependency
after it has finished using the information itself. To support
this, an Abuild.interface
file may specify
additional interface files that are not to be read until after
the item is built. The values in any such files are not
available to the build item itself, but they are available to any
items that depend on the build item that exports this interface.
Such files may be dynamically generated (such as with
autoconf; see Section 18.3, “Autoconf Example”), or they may be hand-generated
files that are just intended not to apply to the build of the
current build item (see Section 27.1, “Opaque Wrapper Example”).
By default, once a variable is declared and assigned to in a
build item's Abuild.interface
, the
declaration and assignments are automatically visible to all
build items that depend on the item that made the declaration or
assignment. In this sense, abuild variables are said to be
recursive. It is also possible to declare
a variable as non-recursive, in which case
assignments to the variable are only visible in the item itself
and in items that depend directly on the
item that makes the assignment. Declarations inherit normally.
[31]
It is also possible to declare an interface variable as
local. When a variable is declared as
local, the declaration and assignment are not visible to any
other build items. This can be useful for providing values only
to the current build item or for using variables to hold
temporary values within the Abuild.interface
file and any after-build files that it may explicitly reference.
[29] In this way, abuild's handles line continuation like GNU Make and the C shell. This is different from how the Bourne shell and the C programming language treat line continuation characters: in those environments, a quoted newline disappears entirely. The only time this matters is if there are no spaces at the beginning of a line following a line continuation character. For abuild, make, and the C shell it doesn't matter whether or not space is present at the beginning of a line following a line continuation character, but for C and the Bourne shell, it does.
[30] This syntax restriction is somewhat arbitrary, but it makes it less likely that syntax errors in specifying environment variable references will create hard-to-solve parsing errors in interface files. If this restriction is in your way, you're probably abusing this feature and may need to rethink why you're accessing environment variables to begin with.
[31] The rationale behind using the terms recursive and non-recursive have to do with how these variables are used. Conceptually, when you reference an interface variable, you see all assignments made to it by any of your recursively expanded list of dependencies, i.e., your direct and indirect dependencies. When a variable is declared to be non-recursive, you only assignments made by your direct dependencies. Other terms, such as indirect or non-inheriting would be technically incorrect or slightly misleading. Although there's nothing specifically recursive or non-recursive about how interface variables are used, we feel that this choice of terminology is a reasonable reflection of the semantics achieved.