Chapter 1
FreeBSD System Programming
Prev
top
Next

Copyright(C) 2001,2002,2003,2004 Nathan Boeger and Mana Tominaga

1.1 FreeBSD's make
Make: to fit, intend, or destine by or as if by creating www.m-v.com

A common and essential utility for Unix software development, make is a nifty bookkeeping utility that keeps track of all file dependencies. Managing such project details as dependencies can be very time consuming and may even stall development. Dependencies tracking can become particularly difficult when multiple developers collaborate on a project. Indeed, the proper use of make can help speed up application development and also increase productivity.

Originally designed to manage the maintenance of application builds, you can use make to perform a variety of additional tasks by creating a sequence of Unix shell commands based on a target's dependencies. These dependencies can be defined in any number of ways - including source files to compile, needed libraries, shell commands and other targets.

Make has a litany of flavors, including GNU make and System V make. Not all features discussed below are present in every version of make, and the one to use will depend on your personal taste. We'll focus on the make that ships with FreeBSD (known also as bmake or pmake), epecially how to use it to compile and upgrade a FreeBSD system, known as make world. While we focus on FreeBSD make, everything discussed here will work across the various BSD distributions.

We'll cover basic file layout and the syntax of a Makefile first. If that's too basic for you, skip ahead to the working example at the end of the chapter. (Note that the code samples inline are meant to illustrate our discussion of make targets and dependencies; they're not usually working code.)

Of course, as with any utility, first consult the man page to get a formal overview, details, and a review of make's available command line options. And, as with any utility, the best way to learn make is to use it. Create some small source files (in any language) and then try some of the examples listed. We hope that by the end of this chapter you'll understand not just make's syntactical rules, but also how it works.

1.2 Makefile Layout

Generally, you use make by specifying a targets and its dependencies by giving it a Makefile to read. Once run, make searches for a file named Makefile or makefile, in that order. This Makefile is commonly put in the root directory for a project; to specify an alternative Makefile, enter it on the command line with the -f (filename) option.

make -f  OtherMakefile 


1.3 Syntax

The structure of a Makefile includes four basic lines, all of which can be extended by adding a '\' character to the end and continuing on the next line (similar to shell programming). Comments begin with a pound sign '#', and run until the ending newline.

########################################
# Simple Makefile with comment example #
########################################

# when run, it will just echo hello
all:
   echo "hello"

To compile a project with make, be sure that you have a proper Makefile in your present working directory, and invoke it with one of the following commands:
bash$  make    

bash$ make all 

bash$ make <target name>


1.4 Targets

You can specify targets in any number of ways, but the most common ways are as object files or a project name. Project names should not contain white space or punctuation marks, though this is only a convention; some white space and punctuation marks are tolerated. The name must start at the beginning of a new line and end with a single colon (:), double colon (::), or an exclamation point (!).

myprog:
     <some commands to the compile myprog targer>

another::
     <some commands to the compile another target>

sample!
     <some commands to the compile sample target>

Follow these target names with the needed dependencies including names, variables or other targets. If you have many dependencies, use a '\' character followed by a newline to separate them. All dependencies must be defined within the Makefile or exist as external files, or make won't know how to complete that dependency.

Some examples are:

all: driver.cpp network_class.cpp file_io_class.cpp network_libs.cpp file_io_libs.cpp

all: myprog.o 

myprog.o: 

In the above example all and myprog.o are the targets to make. Notice that myprog.o is both a target and a dependency. Make will first go to myprog.o and execute its commands, then return to 'all' and execute its commands. This sequence is make's basic foundation.

By convention the all: target is the highest of your targets, and is meant to be the root from which make will branch out to find what it needs to complete the all: target. The all: target is not a requirement, though. If it's not there, make will simply chose the first target listed, and only act on that one unless you specify a target on the command line. We suggest you use the all: target in projects that have a central application to be maintained or built; this is a popular convention for targets and will help you avoid mistakes and unnecessary tasks.

The sequence of the dependencies shown in the above example is a simple one. Here's an example of a more complex and flexible set of sequenced dependencies, sans the commands for the specific targets:

all: myprog.o lib

lib: lex

lex:

myprog.o: app.h

Notice in this example, the all: target has 2 dependencies: myprog.o and lib. Both are targets and make will first go to compile myprog.o. When make evaluates myprog.o, there's a dependency on app.h. Although app.h is not defined in this makefile, the file app.h is a header file located in the current directory.

Once the commands for myprog.o are completed, make will return to all:, then process the next dependency, in this case lib. The dependency lib itself has a dependency, lex, so make will first complete lex: before it returns to complete lib.

NOTE: As you can see, these dependencies can be very long and deeply nested. If you have a large Makefile, be sure to keep it well organized, with your targets in order.

1.5 Evaluation Rules

Dependencies are evaluated by strict rules which depend on how the target names end. Once make decides that the rules are met make will create that specific target by execution of the associated commands (i.e. compile that target ). For example, the advantage of using the single colon: is more fine grained control over which targets would need to be compiled. That is, you could specify if a specific target file needed to be re-compiled every time or only if its source files are out-dated. The rules are based on the ending characters, as below.

If the target name ends with a single colon (:) that target will be created by the following two rules:

  1. If the target does not exist, as with the target named all: in our example above, make will create it.
  2. If any of the source files have a more recent timestamp than the current target. In our examples above the myprog.o would be made if the apps.h or the myprog.c file had a more recent timestamp. This is easy to force by using the touch command
    touch myprog.c

If the target name ends with a double colon (::) it will be created by the following three rules.

  1. If any of the source files have timestamps that are more recent than the current target.
  2. The target does not exist.
  3. The target has no source files associated with it.

When targets end with bang (!) the target will always be created once make has created all of its needed dependencies.

You can only use wildcard expressions for targets, ?, *, and []as part of the final component of the target or source and only to describe an existing file. For example:

myprog.[oc]

Also, expressions using curly braces, {}, may not be needed to describe an existing file. For example:

myprog.[oc]

Also, expressions using curly braces, {}, may not be needed to describe an existing file. As in this example:

{mypgog,test}.o

# the expression above would match myprog.o test.o only

One final note: variable expansions are done in directory order and not alphabetically, as they would be in shell expansions. For example, if you have some type of dependency for your targets based on alphabetical order then the following expansion might not be correct:

{dprog,aprog,bprog,cprog}.cpp


1.6 Variables

Make's ability to use variable is of singular importance. For example, if you have a source file called a.c and for some reason you need to change its name to b.c. Normally, you would then have to re-write every instance of a.c in your make file to b.c. However if you used the following

MYSRC = a.c

Then you would only need to update that single line with the new name, as in:

MYSRC = b.c

You save time, hence meeting the first role of make: Project management.

Variables are referenced by using $( <varliable name> ) or a single $ but this method is not widely used and therefore not recommended.

$(GCC)              = /usr/local/bin/gcc

There are four different classes of variables for make, which we list below in the order that they are searched. (Make's search continues until it reaches the first instance of a value.)

  1. Local values: These are values assigned to a specific target.
  2. Command line: Command line variables are given to make on the command line.
  3. Global: Global variables are assigned inside the Makefile or any included Makefiles. You'll see these most frequently in a Makefile.
  4. Environment: Environment variables are set outside the Makefile in the shell that is running make.

These variable can be defined inside the Makefile with the following five operators:

  1. The equals sign "=" is the most commonly used operator, and similar to those in shell. The values are assigned directly to the variable. For example:
     VAR = < value >
     
  2. The plus equals sign "+=" is the append assignment, where the variable is assigned by appending to the current value. For example:
     VAR += < value to append >
     
  3. The question mark equals sign "?=" is a conditional assignment and will only assign the value if it not yet set. Conditionals are helpful in pre-appending values to a string value. For exampe:
      VAR ?= < value if unset >
      
  4. The colon equals sign ":=" is the expansion assignment. The value will be expanded before assigning it to the value given; the expansion is normally done when the value is referenced. For example:
      VAR := < value to expand >
      
  5. The bang equals sign "!=" is a shell command assignment. The value will be assigned the result of the command, after it is expanded and sent to the shell for execution. If the result has new lines they will be replaced with spaces.For example:
      VAR != < shell command to execute >
      

NOTE: Some variables are defined by an external system wide Makefile (found in /etc/make.conf or /etc/defaults/make.conf ). If you run into problems trying to set a variable from the environment, check these system wide file settings.
A full example is:

  #####################
  # Example make file #
  #####################
  CURDIR != pwd
  CFLAGS ?= -g
  CFLAGS += -Wall -O2
  
  all:
      echo $CFLAGS 
  #######################
  
  bash$ CFLAGS="-g -Wall" make
  

In the example above, the CURDIR is set to the result from the shell command pwd. (Note that the backtick command (i.e. ``) for these assignments is not needed.) The CFLAGS value first will be set to -g only if it's unset, and then it will be append -Wall -O2 regardless of its current value.

1.7 Commands

Make is nothing without commands, and it is only by passing commands to make that it can perform its tasks. Make can only execute the commands and evaluate its success if the commands are successful, based on the shell's exit status evaluation. So, if the command fails and the shell returns an error, then make will quit with an error and stop at that point. Make can be thought of as just another command - it can't actually interact with other commands other than to run them.

Commands must be associated with a target, and any target can have multiple commands. For example:

# example for an all-install target

all-install:
   $(CC) $(CFLAGS)  $(MYSRC)
   cp  $(MYPROG) $(INSTALL_DIR)
   echo "Finished the build and install"

Each command must follow a target on a new line and must have a tab before the command line starts, as shown above.

For the most part, commands can be anything that is a valid shell command, and commands often include variables. For example:

CPP = -g++
CFLAGS = -Wall -O2

myprog.o 
    $(CPP) $(CFLAGS) -c myprog.c

The example below tells make to compile myprog.c using the given values. These commands can be longer than one line and can be written to perform other tasks. This is important because compilers can take a number of command line options, environment settings, defines and so on, like so:

CLFAGS = $(LINK_FLAGS) $(LINK_LIBS) $(OTHER_LIBS) \ 
$(OPTIMIZER_FLAGS) $(DEFINES) $(NO_KERNEL) $(OBJS) \ 
$(CPU_TYPE_FLAGS) $(USE_MY_MALLOC) $(UDEFINES) \
$(SRC_FILE)  $(OTHER_SRC_FILES)


The example below tells make to remove all the object files, core files and the application, then to move the log file over, which is handy.

CPP = -g++
CFLAGS = -Wall -O2
APP = myapp
DATE != date +%m%d%y_%H_%M
LOG = debug


myprog.o:
      $(CPP) $(CFLAGS) -c myprog.c
      rm -f  *.o  *.core $(APP)
      mv  $(LOG).log  $(LOG)_$(DATE).log

clean
    rm -f *.o *.core $(APP)
    mv $(LOG).log $(LOG)_$(DATE).log

However if there's no log file, then make will error out and quit. To avoid this, precede the command with a "-" sign. By adding a minus sign before a command you tell make to execute that command and to ignore errors. (Make will still print the error though.) Thus, make will continue even if a command has an error after execution. For example:

clean
    -rm -f *.o *.core $(APP)
    -mv $(LOG).log $(LOG)_$(DATE).log

This would cause make to ignore the rm and mv errors if they occured.

You can also tell make to suppress the output of commands like the 'echo' shell command. Echo tells make to print out the entire command including the echo statement and then to run the command and print the string to the screen:

    echo $(SOME_DEFINED_STRING)

To avoid this place an at symbol "@" in front of the echo command, which tells make to print out only the string, like so:

     @echo $(SOME_DEFINED_STRING) 

Both the "-" and "@" operators can be used with variables as well as commands as strings, but make sure you properly reference the variable command. Here's how to use the @ operator for commands:

ECHO = echo
MESSAGE = "Print this message"

msg::
  @$(ECHO) $(MESSAGE)  


1.8 Conditional Statements (#if, #ifndef etc.. )

If you're familiar with C and C++, you already know about conditional pre-processor directives. The versatile make has a similar feature. The conditional statements let you choose which parts of the Makefile are processed. These conditional statements can be nested up to a depth of 30 and can be placed anywhere inside the Makefile. Each statement must be prefixed with a period (.) and the conditional block must end with a .endif .

The conditional statements allow for logical operators, logical AND '&&', logical OR '||' and the entire statement can be negated with the '!' operator. The '!' operator has the highest precedence, followed by logical AND and lastly logical OR. Parentheses can be used to specify the order of precedence. Relational operators are also available '>', '>=', '<', '<=', '==' and '!='. These operators can be used on decimal and hexadecimal values. For strings the '==', and '!=' operators can be used. If no operator is given, the value is compared with 0.

In the example below, if the VER variable has been assigned then the conditions are tested. Note that if the VER variable is not assigned then the last .else clause will evaluate to true and assign TAG the value of 2.4_stable.

.if $(VER) >= 2.4
  TAG = 2.4_current
.elif $(VER) == 2.3
  TAG = 2.3_release
.else
  TAG = 2.4_stable
.endif

Conditional statements can test variables or can be used with the following function style expressions.

Some of these have short hand notations. We list the short hand notations for compatibility issues. The long hand is much more implicit and widely understood, but takes longer to type.

When using short hand notations, you don't need the parentheses. Also, the short hand can be mixed with if / else statements along with other short hand statements:

make( < arg > ) short hand [ .ifmake, .ifnmake, .elifmake, .elifnmake ]

.if make(debug)
  CFLAGS += -g
.elif make(production)
  CFLAGS += -O2
.endif

In the example above, make will take a target name as its argument. The value will be true if the target was given on the command line or if its the default target to make. The example below will assign the values to the CFLAGS according to the rules of the make() expression:

Here's the same code in short hand notation:

.ifmake debug
  CFLAGS += -g
.elifmake production
  CFLAGS += -O2
.endif
target( < arg > )

This will take a target name as its argument. The value will be true only if the target has been defined. There isn't a short hand notation for this expression. For example:

.if target(debug)
  FILES += $(DEBUG_FILES)
.endif

The example above will append to the FILES variable if the debug target returns true.

empty ( < arg > )

This will take a variable for its argument and allows for possible modifiers. The value is true when the variable expansion would result in an empty string. There is no short hand for this expression. Also note that you don't need to reference the value when using the expression, VAR not $(VAR). For example:

.if empty (CFLAGS)
   CFLAGS = -Wall -g
.endif


defined( < arg > ) short hand [ .ifdef , .ifndef , .elifdef, elifndef ]

The following example will take a variable for its argument. The value is true only if the variable has been defined:

.if defined(OS_VER)
  .if $(OS_VER) == 4.4
     DIRS += /usr/local/4.4-STABLE_src
  .endif
.else
  DIRS += /usr/src
.endif

Here's the short hand notation:

.ifdef OS_VER
. if $(OS_VER) == 4.4
  DIRS += /usr/local/4.4-STABLE_src
. endif
.else
  OS_VER = 3.2
  DIRS += /usr/src
.endif

As you can see, make allows for both nested conditionals and the define expression. Unlike C, indentations of if statements and variable assignments are not allowed. For visual clarity of your conditional blocks, you can have white space after the period, and before the if. Here's an example:

.if $(DEBUG) == 1
   $(CFLAGS) = -g
.     ifndef $(DEBUG_FLAGS)
         $(FLAGS) = $(DEBUG_FLAGS)
.     endif
.endif


exists( < arg > )

The example below shows how to use exists and how to add the conditional statements to a target. If the tmp directory exists then make will run the -rm tmp/*.o commands. As you can see in this example, the .if statements will only be evaluated in the clean target; the commands run must follow the normal command syntax.

clean:
    -rm -f *.o *.core
.if exists(tmp)
    -rm tmp/*.o
.endif


1.9 System Make Files, Templates, and the .include Directive

One great feature of C is the manifest pre-processor directive, better know as the #include. This feature has been implemented in make, too. The difference is, if you want to include another Makefile then you should include it at the bottom and not at the top like in C, because variables are assigned in order. Makefiles in Makefiles can get confusing, the basic syntax to include a Makefile is simple. Note that the period (.) must precede the word include. The basic syntax is:

.include file name 

To specify a Makefile located in the system make directory, use angle brackets:

.include <file name>

To specify files located in the current directory or one specified with the -I command line option, use double quotes, similar to the C #include:

.include "file name" 

In the following example, if the project.mk has the variable CFLAGS defined, it will be overwritten by the newer declaration. This can cause a bit of trouble when including more than one Makefile.

.include "../project.mk"

CFLAGS = -g

FreeBSD systems have a number of system Makefiles that can be included, with routines for various tasks. On most FreeBSD systems you'll find them in the /usr/share/mk/ directory. However, there is a /etc/defaults/make.conf which can be overwritten by /etc/make.conf. (See man make.conf for more details.)

This is a brief listing of a few that are commonly used. If you are going to heavy kernel programming or applications porting, use and take advantage of Makefile. They're in the form of bsd.<type>.mk, where the <type> stands for what it is used for.

The good thing about using the .include directive is that you can break your project Makefile in pieces. For example, your a project could have a main make file that is included into all the other sub-make files. This main make file could contain the compiler variable along with its needed flags. That way each make file would not need to specify the compiler or needed flags, and simply reference the named compiler. These commonly used pieces can then be used in other Makefile and modification to these routines will then be common across all Makefile.

1.10 Advanced Options

Advanced make options exist for further flexibility. We suggest a thorough read of the make man page for a deep understanding of make. The following options are the ones we use most often.

Local variables

Make has local variables that are specifically defined and only have scope within the current target. These seven are listed below with their system V compatible older notation. (The system V older notation isn't recommended; we only list it for backward compatibility.)

This variables value will be the name of the target:

.TARGET
old style notation: '@'

This variable contains the list for all the sources of this current target:

.ALLSRC
old style notation: '>'

This variable is the implied source for this target. Its value is the name and path of the source file for the target. (This will be demonstrated in the .SUFFIX section below.)

.IMPSRC
old style notation: '<'

This variable holds the list of sources that have been determined to be out of date:

.OODATE
old style notation: '?'

This variables value is the file name with out the suffix or path:

.PREFIX
old style notation: '*'

This variables value will be the name of the archive file:

.ARCHIVE
old style notation: '!'

This variables value is the name of the archive member:

.MEMBER
old style notation: '%'


When using these local variables in dependency lines, only .TARGET, .PREFIX, .ARCHIVE, and .MEMBER may have values for that target.

Another nifty directive is:

.undef <variable>

This is handy when you want to undefine a variable. Note that only global variables can be undefined. For example:

.ifdef DEBUG
.undef RELEASE
.endif


1.11 Transformation Rules (suffix rules)

Transformation rules specify how a target is to be created. You can use these rules - to save time in writing a rule for each object file. The syntax is simple: '.SUFFIXES: (suffix list)

Note that several different suffixes can share the same transformation suffix (.cpp, .cc, .c can all produce a .o transformation). If no suffixes are listed then make will delete all previous ones. This can be very confusing is you have multiple .SUFFIXES rules; we advise using only one and keeping it at the bottom of the Makefile. Consider what's listed inside the .SUFFIXES block as similar to targets to understand its structure:

.SUFFIXES: .cpp .c .o .sh

.c.o:
     $(CC) $(CFLAGS) -c ${.IMPSRC}

.cpp.o:
     $(CPP) $(CXXFLAGS) -c ${.IMPSRC}

.sh:
     cp ${.IMPSRC} ${.TARGET}
     chmod +x ${.TARGET}

These rules listed above will compile the C and C++ source. However for the .sh: rule will also tell make how to create the shell script as well. Note that listing a shell script as a target in this example will require it to be listed with out the .sh extension; however, the shell script must exist with a .sh suffix.

If we list the install_script as a target without the .sh suffix as a dependency, there should be a shell script called install_script.sh with the proper .sh suffix. That is, if a file can be listed as a target and as long as that file exists, then make will only create the targets that it deems out of date with that file, and make will not create that file. The example below illustrates this; for more information, see the example for apps.h:

all: install_script $(OBJS)
     $(CPP) $(CFLAGS) -o $(APP) $(OBJS) $(LINK)

1.12 Useful Command Line Options

Here is a list of a few command line options that are very handy to know and use. This is not a full listing, so refer to the man page for others.

-D <variable name to define>

This option will define a variable from the command line, which is handy when you have .ifdef statements in your Makefile. For example:

.ifdef DEBUG
  CFLAGS += -g -D__DEBUG__
.endif

Then when you run the command make -D DEBUG, make will set the proper CFLAGS and compile your application with your debug statements.

-E < variable name to override >

This option will override the Makefile variable assignment with the environment value instead. Before you use this option be sure to set the environment variable according to your shell. For example:

bash $ CFLAGS="-O2 -Wall" make -E CFLAGS 

-e

Similar to its capitalized counterpart, -e will override all variables inside the Makefile with the environment values. If no environment variables are defined, however, then the values will be assigned normally.

-f <makefile to use>

This will allow you to specify the Makefile on the command line, helpful if you need multiple Makefiles. For example:

 bash$  make -f Makefile.BSD

-j < number of max_jobs >

This flag allows you to specify how many jobs make can spawn. Normally make spawns only one, but for a very large project and to make your hardware earn its keep, use four, as in:

make -j 4 world 

If you exceed four, sometimes it'll take longer to execute, though it may be entertaining to some to watch the CPU spike with six or more jobs specified.

-n

Handy when debugging a Makefile, this option allows you to find out exactly which commands make will be running without make actually executing them. For large projects with many commands, redirect the output to an external file or all the commands will just blow by. Here's how:

bash $ make -n >> make_debug.log 2>&1


-V < variable name >

Using this option will print the variables value, based on the global context. Also make will not build any targets. You can specify multiple -V options on the command line, as in:

make -V CFLAGS -V LINK -V OBJS

1.13 A final example

The Makefile listed below, is an example of a reusable Makefile. When you include it, it will know how to compile C++ source files from the .SUFFIXES rules listed. It will also know how to install the application and clean up the development directory. By no means is this a very comprehensible Makefile, but a good example of creating a generic template style Makefile that can contain common routines for development. This will not only save time from having to retype these common rules over for every Makefile created, but will allow the developer to reuse known good routines as well.

########################################################
#
# FILE: Makefile
#
# AUTHOR: Nathan Boeger
#
# NOTES:
#  This is a generic Makefile for *BSD make, you will
#  need to customize the listed variables below inside
#  the Makefile for your application.
#
# INSTALL_DIR = name of the directory that you want to install
#   this applicaion (Ex: /usr/local/bin/ )
#
# APP          = name of the application
#
# C_SRC      = C source files (Ex: pstat.c )
#
# CPP_SRC  = CPP source files (Ex: node.cpp)
#
#
# $Id: ch01.html,v 1.5 2004/08/10 14:41:39 nathan Exp $
#########################################################

# Make the OBJ's from our defined C & C++ files
.ifdef CPP_SRC
OBJS            =       ${CPP_SRC:.cpp=.o}
.endif

.ifdef C_SRC
OBJS            +=      ${C_SRC:.c=.o}
.endif

# define the  Compiler. The compiler flags will be appended to
# if defined, else they are just assigned the values below
CPP             =        g++
CFLAGS          +=       -Wall -Wmissing-prototypes -O
LINK            +=       -lc

# Add a debug flag.
.ifdef DEBUG
  CFLAGS += -g
.endif

# Targets
all: ${OBJS}
    $(CPP) $(CFLAGS) -o $(APP) ${OBJS} $(LINK)

depend:
    $(CPP) -E -MM ${C_SRC} ${CPP_SRC}  > .depend

#######################################################
#
#        INSTALL SECTION
#
# install will copy the defined application (APP) into the
# directory INSTALL_DIR and chmod it 0755
# for more information on install read MAN(1) install
########################################################
install: all
    install -b -s $(APP) $(INSTALL_DIR)
  
clean
     rm -f $(APP) *.o *.core

# SUFFIX RULES
.SUFFIXES: .cpp .c .o

.c.o:
       $(CPP) $(CFLAGS) -c ${.IMPSRC}
.cpp.o:
       $(CPP) $(CFLAGS) -c ${.IMPSRC}

The Makefile listed below is what you would need to create inside your project directory.

#######################################################
#       PROJECT Makefile
#
# This is what the programs makefile would look like
# These are the only variables you will need to define
######################################################

APP            = myapp
C_SRC          = debug_logger.c
CPP_SRC        = myapp.cpp  base_classes.cpp
INSTALL_DIR    = /usr/local/bin/

# And include the template Makefile, make sure its
# path is correct.  
 
.include "../../bsd_make.mk"


Prev
top
Next
Chapter 1
FreeBSD System Programming