Managing Projects with GNU Make (Nutshell Handbooks)

     

Following a make target, lines whose first character is a tab are assumed to be commands (unless the previous line was continued with a backslash). GNU make tries to be as smart as possible when handling tabs in other contexts. For instance, when there is no possible ambiguity, comments, variable assignments, and include directives may all use a tab as their first character. If make reads a command line that does not immediately follow a target, an error message is displayed:

makefile:20: *** commands commence before first target. Stop.

The wording of this message is a bit odd because it often occurs in the middle of a makefile long after the "first" target was specified, but we can now understand it without too much trouble. A better wording for this message might be, " encountered a command outside the context of a target."

When the parser sees a command in a legal context, it switches to "command parsing" mode, building the script one line at a time. It stops appending to the script when it encounters a line that cannot possibly be part of the command script. There the script ends. The following may appear in a command script:

  • Lines beginning with a tab character are commands that will be executed by a subshell. Even lines that would normally be interpreted as make constructs (e.g., ifdef , comments, include directives) are treated as commands while in "command parsing" mode.

  • Blank lines are ignored. They are not "executed" by a subshell.

  • Lines beginning with a # , possibly with leading spaces (not tabs!), are makefile comments and are ignored.

  • Conditional processing directives, such as ifdef and ifeq , are recognized and processed normally within command scripts.

Built-in make functions terminate command parsing mode unless preceded by a tab character. This means they must expand to valid shell commands or to nothing. The functions warning and eval expand to no characters .

The fact that blank lines and make comments are allowed in command scripts can be surprising at first. The following lines show how it is carried out:

long-command: @echo Line 2: A blank line follows @echo Line 4: A shell comment follows # A shell comment (leading tab) @echo Line 6: A make comment follows # A make comment, at the beginning of a line @echo Line 8: Indented make comments follow # A make comment, indented with leading spaces # Another make comment, indented with leading spaces @echo Line 11: A conditional follows ifdef COMSPEC @echo Running Windows endif @echo Line 15: A warning "command" follows $(warning A warning) @echo Line 17: An eval "command" follows $(eval $(shell echo Shell echo 1>&2))

Notice that lines 5 and 10 appear identical, but are quite different. Line 5 is a shell comment, indicated by a leading tab, while line 10 is a make comment indented eight spaces. Obviously, we do not recommend formatting make comments this way (unless you intend entering an obfuscated makefile contest). As you can see in the following output, make comments are not executed and are not echoed to the output even though they occur within the context of a command script:

$ make makefile:2: A warning Shell echo Line 2: A blank line follows Line 4: A shell comment follows # A shell comment (leading tab) Line 6: A make comment follows Line 8: Indented make comments follow Line 11: A conditional follows Running Windows Line 15: A warning command follows Line 17: An eval command follows

The output of the warning and eval functions appears to be out of order, but don't worry, it isn't. (We'll discuss the order of evaluation later this chapter in Section 5.5.) The fact that command scripts can contain any number of blank lines and comments can be a frustrating source of errors. Suppose you accidentally introduce a line with a leading tab. If a previous target (with or without commands) exists and you have only comments or blank lines intervening , make will treat your accidental tabbed line as a command associated with the preceding target. As you've seen, this is perfectly legal and will not generate a warning or error unless the same target has a rule somewhere else in the makefile (or one of its include files).

If you're lucky, your makefile will include a nonblank, noncomment between your accidental tabbed line and the previous command script. In that case, you'll get the "commands commence before first target" message.

Now is a good time to briefly mention software tools. I think everyone agrees, now, that using a leading tab to indicate a command line was an unfortunate decision, but it's a little late to change. Using a modern, syntax-aware editor can help head off potential problems by visibly marking dubious constructs. GNU emacs has a very nice mode for editing makefile s. This mode performs syntax highlighting and looks for simple syntactic errors, such as spaces after continuation lines and mixing leading spaces and tabs. I'll talk more about using emacs and make later on.

5.1.1 Continuing Long Commands

Since each command is executed in its own shell (or at least appears to be), sequences of shell commands that need to be run together must be handled specially. For instance, suppose I need to generate a file containing a list of files. The Java compiler accepts such a file for compiling many source files. I might write a command script like this:

.INTERMEDIATE: file_list file_list: for d in logic ui do echo $d/*.java done > $@

By now it should be clear that this won't work. It generates the error:

$ make for d in logic ui /bin/sh: -c: line 2: syntax error: unexpected end of file make: *** [file_list] Error 2

Our first fix is to add continuation characters to each line:

.INTERMEDIATE: file_list file_list: for d in logic ui \ do \ echo $d/*.java \ done > $@

which generates the error:

$ make for d in logic ui \ do \ echo /*.java \ done > file_list /bin/sh: -c: line 1: syntax error near unexpected token `>' /bin/sh: -c: line 1: `for d in logic ui do echo /*.java make: *** [file_list] Error 2

What happened ? Two problems. First, the reference to the loop control variable, d , needs to be escaped. Second, since the for loop is passed to the subshell as a single line, we must add semicolon separators after the file list and for-loop statement:

.INTERMEDIATE: file_list file_list: for d in logic ui; \ do \ echo $$d/*.java; \ done > $@

Now we get the file we expect. The target is declared .INTERMEDIATE so that make will delete this temporary target after the compile is complete.

In a more realistic example, the list of directories would be stored in a make variable. If we are sure that the number of files is relatively small, we can perform this same operation without a for loop by using make functions:

.INTERMEDIATE: file_list file_list: echo $(addsuffix /*.java,$(COMPILATION_DIRS)) > $@

But the for-loop version is less likely to run up against command-line length issues if we expect the list of directories to grow with time.

Another common problem in make command scripts is how to switch directories. Again, it should be clear that a simple command script like:

TAGS: cd src ctags --recurse

will not execute the ctags program in the src subdirectory. To get the effect we want, we must either place both commands on a single line or escape the newline with a backslash (and separate the commands with a semicolon):

TAGS: cd src; \ ctags --recurse

An even better version would check the status of the cd before executing the ctags program:

TAGS: cd src && \ ctags --recurse

Notice that in some circumstances omitting the semicolon might not produce a make or shell error:

disk-free = echo "Checking free disk space..." \ df . awk '{ print $ }'

This example prints a simple message followed by the number of free blocks on the current device. Or does it? We have accidentally omitted the semicolon after the echo command, so we never actually run the df program. Instead, we echo:

Checking free disk space... df .

into awk which dutifully prints the fourth field, space.. ..

It might have occurred to you to use the define directive, which is intended for creating multiline command sequences, rather than continuation lines. Unfortunately, this isn't quite the same problem. When a multiline macro is expanded, each line is inserted into the command script with a leading tab and make treats each line independently. The lines of the macro are not executed in a single subshell. So you will need to pay attention to command-line continuation in macros as well.

5.1.2 Command Modifiers

A command can be modified by several prefixes. We've already seen the "silent" prefix, @ , used many times before. The complete list of prefixes, along with some gory details, are:

@

Do not echo the command. For historical compatibility, you can make your target a prerequisite of the special target .SILENT if you want all of its commands to be hidden. Using @ is preferred, however, because it can be applied to individual commands within a command script. If you want to apply this modifier to all targets (although it is hard to imagine why), you can use the ”silent (or -s ) option.

Hiding commands can make the output of make easier on the eyes, but it can also make debugging the commands more difficult. If you find yourself removing the @ modifiers and restoring them frequently, you might create a variable, say QUIET , containing the @ modifier and use that on commands:

QUIET = @ hairy_script: $(QUIET) complex script ...

Then, if you need to see the complex script as make runs it, just reset the QUIET variable from the command line:

$ make QUIET= hairy_script complex script ...

-

The dash prefix indicates that errors in the command should be ignored by make . By default, when make executes a command, it examines the exit status of the program or pipeline, and if a nonzero (failure) exit status is returned, make terminates execution of the remainder of the command script and exits. This modifier directs make to ignore the exit status of the modified line and continue as if no error occurred. We'll discuss this topic in more depth in the next section.

For historical compatibility, you can ignore errors in any part of a command script by making the target a prerequisite of the .IGNORE special target. If you want to ignore all errors in the entire makefile, you can use the ”ignore-errors (or -i ) option. Again, this doesn't seem too useful.

+

The plus modifier tells make to execute the command even if the ”just-print (or -n ) command-line option is given to make . It is used when writing recursive makefile s. We'll discuss this topic in more detail in the Section 6.1 in Chapter 6.

Any or all of these modifiers are allowed on a single line. Obviously, the modifiers are stripped before the commands are executed.

5.1.3 Errors and Interrupts

Every command that make executes returns a status code. A status of zero indicates that the command succeeded. A status of nonzero indicates some kind of failure. Some programs use the return status code to indicate something more meaningful than simply "error." For instance, grep returns 0 (success) if a match is found, 1 if no match is found, and 2 if some kind of error occurred.

Normally, when a program fails (i.e., returns a nonzero exit status), make stops executing commands and exits with an error status. Sometimes you want make to continue, trying to complete as many targets as possible. For instance, you might want to compile as many files as possible to see all the compilation errors in a single run. You can do this with the ”keep-going (or -k ) option.

Although the - modifier causes make to ignore errors in individual commands, I try to avoid its use whenever possible. This is because it complicates automated error processing and is visually jarring.

When make ignores an error it prints a warning along with the name of the target in square brackets. For example, here is the output when rm tries to delete a nonexistent file:

rm non-existent-file rm: cannot remove `non-existent-file': No such file or directory make: [clean] Error 1 (ignored)

Some commands, like rm , have options that suppress their error exit status. The -f option will force rm to return success while also suppressing error messages. Using such options is better than depending on a preceding dash.

Occasionally, you want a command to fail and would like to get an error if the program succeeds. For these situations, you should be able to simply negate the exit status of the program:

# Verify there are no debug statements left in the code. .PHONY: no_debug_printf no_debug_printf: $(sources) ! grep --line-number '"debug:' $^

Unfortunately, there is a bug in make 3.80 that prevents this straightforward use. make does not recognize the ! character as requiring shell processing and executes the command line itself, resulting in an error. In this case, a simple work around is to add a shell special character as a clue to make :

# Verify there are no debug statement left in the code .PHONY: no_debug_printf no_debug_printf: $(sources) ! grep --line-number '"debug:' $^ < /dev/null

Another common source of unexpected command errors is using the shell's if construct without an else .

$(config): $(config_template) if [ ! -d $(dir $@) ]; \ then \ $(MKDIR) $(dir $@); \ fi $(M4) $^ > $@

The first command tests if the output directory exists and calls mkdir to create it if it does not. Unfortunately, if the directory does exist, the if command returns a failure exit status (the exit status of the test), which terminates the script. One solution is to add an else clause:

$(config): $(config_template) if [ ! -d $(dir $@) ]; \ then \ $(MKDIR) $(dir $@); \ else \ true; \ fi $(M4) $^ > $@

In the shell, the colon (:) is a no-op command that always returns true, and can be used instead of true . An alternative implementation that works well here is:

$(config): $(config_template) [[ -d $(dir $@) ]] $(MKDIR) $(dir $@) $(M4) $^ > $@

Now the first statement is true when the directory exists or when the mkdir succeeds. Another alternative is to use mkdir -p . This allows mkdir to succeed even when the directory already exists. All these implementations execute something in a subshell even when the directory exists. By using wildcard , we can omit the execution entirely if the directory is present.

# $(call make-dir, directory) make-dir = $(if $(wildcard ),,$(MKDIR) -p ) $(config): $(config_template) $(call make-dir, $(dir $@)) $(M4) $^ > $@

Because each command is executed in its own shell, it is common to have multiline commands with each component separated by semicolons. Be aware that errors within these scripts may not terminate the script:

target: rm rm-fails; echo But the next command executes anyway

It is best to minimize the length of command scripts and give make a chance to manage exit status and termination for you. For instance:

path-fixup = -e "s;[a-zA-Z:/]*/src/;$(SOURCE_DIR)/;g" \ -e "s;[a-zA-Z:/]*/bin/;$(OUTPUT_DIR)/;g" # A good version. define fix-project-paths sed $(path-fixup) > .fixed && \ mv .fixed endef # A better version. define fix-project-paths sed $(path-fixup) > .fixed mv .fixed endef

This macro transforms DOS-style paths (with forward slashes ) into destination paths for a particular source and output tree. The macro accepts two filenames, the input and output files. It is careful to overwrite the output file only if the sed command completes correctly. The "good" version does this by connecting the sed and mv with && so they execute in a single shell. The "better" version executes them as two separate commands, letting make terminate the script if the sed fails. The "better" version is no more expensive (the mv doesn't need a shell and is executed directly), is easier to read, and provides more information when errors occur (because make will indicate which command failed).

Note that this is a different issue than the common problem with cd :

TAGS: cd src && \ ctags --recurse

In this case, the two statements must be executed within the same subshell. Therefore, the commands must be separated by some kind of statement connector, such as ; or && .

5.1.3.1 Deleting and preserving target files

If an error occurs, make assumes that the target cannot be remade. Any other targets that have the current target as a prerequisite also cannot be remade, so make will not attempt them nor execute any part of their command scripts. If the ”keep-going (or -k ) option is used, the next goal will be attempted; otherwise , make exits. If the current target is a file, it may be corrupt if the command exits before finishing its work. Unfortunately, for reasons of historical compatibility, make will leave this potentially corrupt file on disk. Because the file's timestamp has been updated, subsequent executions of make may not update the file with correct data. You can avoid this problem and cause make to delete these questionable files when an error occurs by making the target file a prerequisite of .DELETE_ON_ERROR . If .DELETE_ON_ERROR is used with no prerequisites, errors in any target file build will cause make to delete the target.

A complementary problem occurs when make is interrupted by a signal, such as a Ctrl-C. In this case, make deletes the current target file if the file has been modified. Sometimes deleting the file is the wrong thing to do. Perhaps the file is very expensive to create and partial contents are better than none, or perhaps the file must exist for other parts of the build to proceed. In these cases, you can protect the file by making it a prerequisite of the special target .PRECIOUS .

Категории