Using Jam
Originally, I was willing to write “Introduction to Jam”. But Jam documentation seems to do the job quite well already, so I’ve changed my plan to write some important things as I remember. This document does not cover most syntax and rules and is not for first learners. It contains an overview of very basic concepts and quite detailed problems
Hello, world!
Make Jamfile. In simple case, the following one line is enough.
Run jam.
Here’s some background knowledge.
- Jam binary contains a text called Jambase which is described in jam syntax.
- Jambase defines some standard rules, including
Mainabove. (kind of a standard library) - Jam starts with interpreting Jambase.
- Jambase ends with including Jamfile on the current directory.
Whitespace
Every tokens, including colons and semicolons, should be separated by whitespace. For example,
is correct, but
or,
is not.
This is because jam internally uses a very simple and fast custom scanner instead of lex. A few small advantages are
- String including separators can be expressed directly. (Separator itself can be expressed between double quotations. e.g.,
":") - (Like python’s indent) Sentences becomes very regular.
But, it’s very fragile to many mistakes, especially for beginners. Many times it’s hard to find this error because omitting semicolon makes the entire next sentence to be part of the last argument, and an error message pops up at some unrelated place far away. Lightness I like, but I doubt that this was really necessary. Anyway, after getting used to, regular syntax does helps readability.
Meaning of the one line
Here,
Mainis rule name defined in Jambase. Above line is an invocation of theMainrule.- Arguments are separated by colons.
progis the first argument, andmain.c data.cis the second one. - Words separated by whitespace becomes a list of words, instead of a single string containing whitespace.
That is, the second argument is a list composed of
main.canddata.c.
Compare above line with the following Makefile counterpart.
cc -o prog main.o data.o
main.o: main.c common.h
cc -c -o main.o main.c
data.o: data.c common.h
cc -c -o data.o data.c
Surely, Main have to be a rule that defines relations among prog, main.c and data.c just like above Makefile.
Let’s translate the first linking rule of Makefile to jam.
actions Link { cc -o $(1) $(2) }
Link prog : main.o data.o ;
This is an example of rules and actions which is the building block of jam. Comparing with Makefile reveals the intentions.
- A rule is for expressing a pattern of dependencies and variable settings.
- An action is for expressing a pattern of command executions.
- Invocation of a rule = designation of an action (One of them may not exist. It’s an error when both are missing.)
- An action is executed when the targets in the first argument are to be updated by dependency.
- When several actions are designated, all of them are executed.
- Action definition can reference the first and the second argument, not others.
- All arguments are not directly related to dependency.
- It is a built-in rule
Dependsthat gives the dependency.
By defining Link as above, one can reuse the same pattern for other targets.
(Personally, this relation between the rule and the action was what I’ve missed and confused most while learning jam.)
An abridged version of entire rules and actions for Main is as below.
MainFromObjects $(prog) : $(srcs:S=.o) ;
Objects $(srcs) ;
}
rule MainFromObjects prog : objs {
Depends $(prog) : $(objs) ;
Link $(prog) : $(objs) ;
}
actions Link { cc -o $(1) $(2) }
rule Objects srcs { for src in $(srcs) { Object $(src:S=.o) : $(src) ; }
rule Object obj : src { Cc $(obj) : $(src) ; }
rule Cc obj : src { Depends $(obj) : $(src) ; }
actions Cc { cc -c -o $(1) $(2) }
Note that
$(src:S=.o)means a list with suffix of each element of$(src)replaced by .o- Some rules look meaningless because of the omissions, but actually they have their role.
Automatic dependency tracking
If you’ve read carefully, you may have noticed that
previous rules does not gives dependency to common.h from main.o and data.o.
A little less abridged Object rule contains more.
Cc $(obj) : $(src) ;
HDRSCAN on $(src) = "^[ \t]*#[ \t]*include[ \t]*[<\"]([^\"]*)[\">].*$" ; # \t should be actual tab
HDRRULE on $(src) = HdrRule ;
}
rule HdrRule src : hdrs {
Includes $(src) : $(hdrs) ;
HDRSCAN on $(hdrs) = $(HDRSCAN) ;
HDRRULE on $(hdrs) = $(HDRRULE) ;
}
Here, HDRSCAN and HDRRULE are target-specific built-in variables which,
when both set for a target,
- egrep pattern in
HDRSCANis searched for. - A rule named in
HDRRULEis invocated with- Target as the first argument
- Matches to () in the pattern as the second argument
Includes used in HdrRule is a built-in rule similar to Depends.
Includes src : hdr ; makes any target that depends on src also depends on hdr.
(It is main.o, not main.c that depends on common.h.)
As one can see, it’s flexible enough to extend beyond C and C++.
Procedural language
Syntactically, jam somewhat resembles make, but actually writing a few Jamfile’s shows many differences. One of them is that jam is a procedural language unlike make (or c preprocessor, tex, m4) which is a macro language. That is, all variable references, rule invocations and action designations use the variable, rule and action at the time the sentence is evaluated. (Exceptionally, the definitions of rules or actions are not evaluated, until actual invocation or execution.)
For example,
Clearly, A is 1, but (without := of GNU make) make uses 2 because variable expansion is done later.
Because of this, it’s important when variables are set, or rules are invoked.
(Usually, dependency graph does not change unless rules do not change global variables.)
Phases
Jam has four phases of operations.
- Parsing: construct the dependency graph with actions designated.
- Binding: search for a matching file and record the file’s time for each file target in dependency graph.
- Updating: executes the actions of each target that needs an update.
For binding of a relative filename, target-specific built-in variable SEARCH and LOCATE are used.
SEARCH is for searching an existing file, and LOCATE is for locating a generated file.
LOCATE has higher priority. If SEARCH fails, current directory is tried.
The distinction of phases, which seems pretty obvious at first time, bring subtle issues.
Files which does not present at binding phase can’t change the dependency graph even if it is generated at updating phase.
For example, dependencies for header files included in a generated source file (which doesn’t exist yet) is not included, because the header rule is skipped. Sometimes it’s OK, but when one of the included header files is to be generated, it is not generated because of the missing dependency and compilation of the source file will fail.
To handle this, the rule that generates the source file should build the dependencies in advance.
Even if update action does not actually change the target, other targets that depend on it still gets updated, because jam doesn’t bind again after update.
Directory tree
Assume the following directory structure.
- src
- src/common
- src/server
- src/server/tests
- src/client
- src/client/tests
When src is the top-most directory, src/Jamfile should look like:
…
SubInclude TOP common ;
SubInclude TOP server ;
SubInclude TOP client ;
Similarly, src/server/Jamfile (assuming it need, say, a library in common directory) should look like:.
…
SubInclude TOP common ;
SubInclude TOP server tests ;
SubDiris a rule defined in Jambase, and it should be invoked at the beginning.SubDirincludes src/Jamrules if it exists, and sets up variables used in other Jambase rules includingSUBDIRSubIncludeincludes Jamfile in the given directory. Because this Jamfile also should begin withSubDir, related variables are changed afterword. So, sentences exceptSubIncludeafterSubIncludeis invalid ones almost all times.
The variable SUBDIR holds the relative path from the directory jam started to the current Jamfile’s directory.
Grist
Grist is something to prevent different targets with the same name (usually in different directories) from acting as one target. Technically, grist is a hidden name of a target which
- is used like part of target name in comparison.
- is not used for filename at binding phase.
That is, two targets with the same name, but different grists differs.
SubDir sets up a default grist for each directory,
and other Jambase rules always use the grist for targets that need to be bound.
For example, to set a target-specific CFLAGS for main.o,
does not work, but
does.
Epilogue
This document omits many important items for learning jam,
including syntax (control flow, conditional, …), variable expansion & modifier,
some built-in rules and major Jambase rules (like Main, Library, LinkLibrary).
Refer to the following references.
- Jam.html: Jam syntax, built-in rules and built-in variables
- Jambase.html: Jambase rules, targets and variables
- moonz's blog
- Login to post comments
- 한국어