Monthly Columns
 

Managing web sites using Unix

Copyright © 1999 Nik Clayton

Part Three

(Parts one and two can be found here: Part one, Part two)

This is the third of several articles explaining how to use the tools provided by Unix and clones (such as the free BSD implementations, and the various different Linux distributions) to manage the contents of a website, such as the free webspace that ISPs often give to their customers.

There is nothing about the techniques described here (and in future articles) that limit them to small, personal, websites. The author has successfully used these approaches to manage sites with thousands of pages and half a dozen active webmasters working on the site.

This article continues the exploration of the abilities of make(1) from the previous article, and a complete framework for installing a website is built.


Introduction

The previous article introduced you to make(1). You wrote some simple Makefiles to control make(1) and to copy your fledgling web pages from the repository (managed using CVS) to the staging area.

In this article we continue the exploration of make(1) and the creation of several Makefiles to complete the task of copying files to the staging area. You will learn about writing recursive Makefiles and how these can be used to install files from directories, and their sub-directories with just one command. You will also see how to create make(1)'s equivalent of subroutines, and how to separate your Makefiles into one or more libraries of common code.

Note: Do not forget the CVS commands from the first article. You should use them to add and commit the files you write while following this article.

Important: The sample Makefiles in this article are written for Berkeley Make, the default on the BSD systems. Some of these examples are not compatible with other make(1)s, such as GNU Make. Download a copy of Berkeley Make from http://www.quick.com.au/ftp/pub/sjg/help/bmake.html.


Pre-requisites

The sh(1) scripting language. While not essential, this will make it easier for you to follow some of the examples in this article.


The problem

After creating (and committing to CVS) your website in the work area you have to copy the site to the staging directory. The previous article showed how you could do this using a Makefile. To recap, the Makefile you were left with looked like this;

    #
    # Sample Makefile to install multiple files
    #
    
    HTML=  index.html about.html
    
    DESTDIR?= ../mystage
    
    CP=      /bin/cp
    CHMOD=   /bin/chmod
          
    install:
              for htmlfile in ${HTML}; do \
                  ${CP} -f $$htmlfile ${DESTDIR}/$$htmlfile; \
                  ${CHMOD} 444 ${DESTDIR}/$$htmlfile; \
              done

HTML is a list of files to be copied, DESTDIR is the directory they will be copied to, and the install target loops over the list of filenames in HTML and copies each file to the destination directory, and ensures that the files in the destination directory are read-only.

This works for the moment. If you add more files to the directory then you also have to add them to HTML. You can override the choice of destination directory when you run make(1) by redefining DESTDIR on the command line (make DESTDIR=/another/directory install) and you can do the same with the locations of cp(1) and chmod(1)

This does not work as soon as you add directories to your work area (and repository). The Makefile rules do not handle directories, and any files in them will be ignored. This Makefile must be re-written to support sub-directories (and their sub-directories, and so on).


How not to do it

A naive approach might be to include directory names in the filenames listed in HTML. If foo and bar are both directories, each containing an index.html you could write:

    ...
    HTML=  index.html about.html foo/index.html bar/index.html
    
    ...
          
    install:
              for htmlfile in ${HTML}; do \
                  ${CP} $$htmlfile ${DESTDIR}/$$htmlfile; \
                  ${CHMOD} 444 ${DESTDIR}/$$htmlfile; \
              done

This will not work if the subdirectories foo and bar do not exist in DESTDIR. You would need to add checks to intall to ensure that the directories exist.

In addition, if any of the files in foo or bar need manipulating before installing (converting to GIF, ...) then install becomes even more complex.

Fortunately, there is a better approach.


Recursive make(1)

Recursion is a term from mathematics that has been appropriated by computer science. Simplified, a process is recursive if, in order to solve the problem, the original process is called again.

The general principle is as follows; suppose you have have a directory with several sub-directories. Each of these sub-directories may themselves have sub-directories, and so on. Each directory contains a Makefile, and each Makefile provides implementations of certain targets (all, install, clean, and so on).

From the first directory, running a command such as make all will build all the files in the current directory. Then, for each subdirectory, the all target will change in to that subdirectory, and run make all there.

If there are any subdirectories of the first second-level directory then the Makefile in that directory will do the same thing. The process continues, until the bottom of the directory structure is reached. Then the process backs up one level in the directory tree, and tries the next directory at this level. When all directories at this level have been completed, it backs up one more level, and carries on.

Eventually, the process has reached all the way back to the top directory (the one in which make all was first run). The next directory in the list of subdirectories is then entered, and the process continues, until eventually make all has been run in all the subdirectories.

This is best illustrated with some sample code that actually does it.

Make a directory tree somewhere (this can be outside of your work area) that looks like this;

    /
    |-- dir1
    |    |-- dir1.1
    |    |-- dir1.2
    |    `-- dir1.3
    |-- dir2
    |    |-- dir2.1
    |    |     `-- dir2.1.1
    |    `-- dir2.2
    `-- dir3
         `-- dir3.1
    % mkdir dir1 dir1/dir1.1 dir1/dir1.2 dir1/dir1.3
    % mkdir dir2 dir2/dir2.1 dir2/dir2.1/dir2.1.1 dir2/dir2.2
    % mkdir dir3 dir3/dir3.1

In the top level directory, place the following Makefile:

    SUBDIR=dir1 dir2 dir3
    
    all:
            @echo "make all" in `pwd`
            @for subdir in ${SUBDIR}; do \
                    (cd $$subdir; ${MAKE} all); \
            done

Examine this, it is not very different from the ones that have gone before.

SUBDIR contains a list of the subdirectories that this Makefile should process. There is one target, all. This target starts by displaying the name of the directory in which make all is being run[1]. It then loops over each subdirectory listed in SUBDIR, executing the code (cd $$subdir; make all). This will ensure that each directory is entered and make all is run inside.

As you should have realised, ${MAKE} is a reference to a make(1) variable (just like ${SUBDIR} is). But this variable is not defined anywhere in the Makefile, so how does it get a value?

MAKE is one of a limited number of variables that make(1) will define itself (if it is not already defined in the Makefile). It will be set to the name of the make(1) command that is currently processing this Makefile.

This might seem redundant. After all, you know the name of the make(1) command that is processing this file, make.

This is not always the case. If you are running these examples on a BSD system then that line could have been written (cd $$subdir; make all) and it would work, because make is the name of the make(1) program.

If you are running these examples on Linux, (cd $$subdir; make all) would not work, because your Berkeley Make is almost certainly called bmake. Rather than try and code this difference in to the Makefile, it is far simpler to let make(1) do it itself. So on a BSD system MAKE will hold the value make (assuming you typed make to run it) and on a Linux system MAKE will be set to bmake.

Using MAKE helps keep the Makefile portable between different operating systems.

Why are there parentheses around the cd(1) and make(1) commands? Remember that the \ at the end of each of the lines in the for loop makes make(1) treat this as one long line to be passed to the shell, sh(1). This means that any directory changes you make or commands you run affect the current shell.

Placing cd $$subdir; make all in parentheses tells the shell to run these two commands by running another copy of the shell (a sub-shell) and executing the commands inside the sub-shell. This way, any changes made by these two commands (including the change of directory) will not affect the main shell.

It is possible to write complicated code that ensures that the current directory is preserved, that the values of all environment and shell variables are not disturbed, and that other transient changes are correctly restored. But it is much simpler to wrap the problematic commands in parentheses and sidestep any problems.

Copy this Makefile in to each of the other directories.

    % cp Makefile dir1
    % cp Makefile dir1/dir1.1
    % cp Makefile dir1/dir1.2
    % cp Makefile dir2
    % cp Makefile dir2/dir2.1
    % cp Makefile dir2/dir2.1/dir2.1.1
    % cp Makefile dir2.1
    % cp Makefile dir3
    % cp Makefile dir3/dir3.1

Now edit each one of these Makefiles, so that SUBDIR accurately reflects the names of the subdirectories below that Makefile. The Makefiles in dir1/dir1.1, dir1/dir1.2, dir2/dir2.1.1, dir2/dir2.1, and dir3/dir3.1 will have an empty value for SUBDIR.

You can now run make(1) from the top directory to process the first Makefile.

    % make
    make all in /var/tmp/make
    make all in /var/tmp/make/dir1
    make all in /var/tmp/make/dir1/dir1.1
    for subdir in ; do  (cd $subdir; make all);  done;
    /bin/sh: syntax error at line 1: `;' unexpected
    *** Error code 2
    
    Stop.
    *** Error code 1
    
    Stop.
    *** Error code 1
    
    Stop.

This is probably not quite what you expected. We need to investigate what has gone wrong.

The first three lines look correct. The @echo ... line in the all target has been executed, printing the directory that make(1) is currently running in. As you can see, make(1) has correctly descended down the directory tree to dir1/dir1.1 and executed the Makefile in that directory.

It seems that only part of the all target in this Makefile has been run. We can see that the echo ... line has been run, but the @for ... lines are where the problem lies.

make(1) has displayed the line that has the problem;

    for subdir in ; do  (cd $subdir; make all); done;

Notice how although this was three separate lines in the Makefile make(1) has displayed it on one line. This is another artifact of using the \ at the end of the lines to force make(1) to treat it as one long line.

A moment's thought should reveal the problem. As discussed in the previous article, a for loop in the sh(1) scripting language looks like this;

    for variable-name in list-of-items; do
          code that forms the body of the loop
    done

If you examine the code that make(1) printed, you will see that there is no list-of-items. Of course, this is because for this Makefile there are no subdirectories, and so SUBDIR is empty.

Now we know what the problem is we can try and fix it.

The simplest solution would be to just remove the for loop code from each Makefile that has no subdirectories. This is less than optimal; you would then have two different all targets to maintain, and you would need to remember to add the code back should any of these directories have subdirectories created under them.

A far better approach would be to add some code that can recognise when SUBDIR is empty, and skips the for loop entirely. As you might expect, make(1) can do this.

Alter the all target so that it looks like this;

    all:
            @echo "make all" in `pwd`
    .if defined(SUBDIR) && !empty(SUBDIR)
            @for subdir in ${SUBDIR}; do \
                    (cd $$subdir; ${MAKE} all); \
            done
    .endif

Two new lines have been added, both starting with a period (.). Notice how these lines are not indented with a TAB. They must start in the first column. These form a make(1) conditional.

The syntax of the conditional should be easy to understand. If the SUBDIR variable has been defined, and (&&) the variable is not (!) empty then the portion of the rule up to the .endif will be executed. Otherwise it will be ignored.

These conditionals are not the Bourne Shell if conditional. They mean roughly the same thing, but these are built in to make(1). To distinguish them from lines of shell script they all start with a period (.).

Note: If you are not a programmer the && and the ! may seem strange. They are part of a family of connectors, that are used to connect different parts of a logical test. The third member of the family is or, written with two vertical bars (||).

In expressions such as the one above they behave as you would expect from their English names.

make(1) can perform more tests than just defined() and empty(). They will be covered later in this series---or you can look at the make(1) manual page in the Directives, Conditionals, and For loops section.

If you test these changes by running make(1) from the top level directory again you should see this:

    % make
    make all in /var/tmp/make
    make all in /var/tmp/make/dir1
    make all in /var/tmp/make/dir1/dir1.1
    make all in /var/tmp/make/dir1/dir1.2
    /bin/sh: syntax error at line 1: `;' unexpected
    *** Error code 2
    
    Stop.
    *** Error code 1
    
    Stop.
    *** Error code 1
    
    Stop.

As you can see, we've fixed the problem for the Makefile in dir1/dir1.1 but the Makefile in dir1/dir1.2 has the same problem.

Add the .if ... and .endif lines to all the other files (not just those that have no subdirectories), and then rerun the test:

    % make
    make all in /var/tmp/make
    make all in /var/tmp/make/dir1
    make all in /var/tmp/make/dir1/dir1.1
    make all in /var/tmp/make/dir1/dir1.2
    make all in /var/tmp/make/dir2
    make all in /var/tmp/make/dir2/dir2.1
    make all in /var/tmp/make/dir2/dir2.1/dir2.1.1
    make all in /var/tmp/make/dir2/dir2.2
    make all in /var/tmp/make/dir3
    make all in /var/tmp/make/dir3/dir3.1

Success. This is exactly what we set out to achieve.


Reusing code in different Makefiles

We have been successful in constructing a collection of Makefiles with a target that will recurse down the directory tree (as listed in the SUBDIR). We have done this by replicating the body of the all target across 10 different files.

This is obviously unacceptable. Any time you need to make a change to the target you will need to edit 10 files (as you did when adding the .if ... and .endif conditionals), with the associated potential for error (not to mention increased workload). There must be some way of storing fragments of a Makefile in another file, and then including it in other files.

There is. The .include "filename" directive allows you to include one Makefile within another.

The Makefiles that you have written so far can be split in to two parts; a part that changes in each file, and a part that is static. In these files, the line that defines SUBDIR is different in each file, while the all target does not change. We will move the all target in to a separate file, and then include this file in to the other Makefiles.

Create a new file in the top level directory called all.mk. Copy the all target out of one of your existing files and paste it in to this new file. Then edit all the other Makefiles in the directories and subdirectories. Remove the all target in its entirety, and replace it with the line .include "all.mk". Each Makefile should now look like:

    SUBDIR=dir1.1 dir1.2
    
    .include "all.mk"

Obviously the precise contents of SUBDIR will vary from file to file.

After making this change, try and process the top level Makefile:

    % make
    make all in /var/tmp/make
    "Makefile", line 3: Could not find all.mk
    Fatal errors encountered -- cannot continue
    *** Error code 1
    
    Stop.

Another problem. What has gone wrong this time?

We can deduce that the first Makefile must have worked. This is because the first line of output is from the all target, so all.mk must have been successfully included in to the top level Makefile.

So it seems that the Makefile in dir1 has failed. Looking at the error message we can see that it is because it failed to load all.mk. This is probably because the Makefile is in dir1, and all.mk is in the directory above it.

You could fix this by altering the .include line. In Makefiles one directory below it would need to be .include "../all.mk", two directories below it would need to be ../../all.mk, and so on.

While this works, it means that you have embedded same extra information in your Makefiles (namely, their position in the filesystem relative to all.mk) that does not belong there.

Fortunately, you can control which directories make(1) looks in when it processes .include directives. You do this by specifying the -I parameter on the make(1) command line, including the full path to the directory in question.

Suppose that you have put these test files in the directory /var/tmp/make. You can force make(1) to look in this directory for files to include like this:

    % make -I/var/tmp/make
    make all in /var/tmp/make
    make all in /var/tmp/make/dir1
    make all in /var/tmp/make/dir1/dir1.1
    make all in /var/tmp/make/dir1/dir1.2
    make all in /var/tmp/make/dir2
    make all in /var/tmp/make/dir2/dir2.1
    make all in /var/tmp/make/dir2/dir2.1/dir2.1.1
    make all in /var/tmp/make/dir2/dir2.2
    make all in /var/tmp/make/dir3
    make all in /var/tmp/make/dir3/dir3.1

As you can see, the Makefiles are now being processed correctly.


Using this on the web site

We can now use these techniques in the Makefiles that you have for your website in your work area.

Your work area will need to contain at least one subdirectory. You may well have more if you have imported a website that you already maintain, and you can just repeat the steps here in each subdirectory that you have.

Note: In these examples the subdirectory will be called unix, and it contains an index.html. The parent directory (the top of your web site) also contains an index.html as well as about.html. Adjust the filenames included in the examples for your situation as necessary.

This time, the install target will be put in a separate file. The variable definitions for the paths to programs can be put in this file as well. Create a new file in the mywork directory called web.mk, and put the following lines in:

    CP=      /bin/cp
    CHMOD=   /bin/chmod
    
    install:
            @for htmlfile in ${HTML}; do \
                    ${CP} -f $$htmlfile ${DESTDIR}/$$htmlfile; \
                    ${CHMOD} 444 ${DESTDIR}/$$htmlfile; \
            done
    .if defined(SUBDIR) && !empty(SUBDIR)
            @for subdir in ${SUBDIR}; do \
                    (cd $$subdir; ${MAKE} install); \
            done
    .endif

Edit mywork/Makefile so that it looks like this:

    #
    # Sample Makefile to install multiple files in multiple directories
    #
    
    HTML=  index.html about.html
    
    SUBDIR= unix
          
    DESTDIR?= ../mystage
    
    .include "web.mk"

And then add mywork/unix/Makefile to contain this:

    HTML=  index.html
    
    .include "web.mk"

As normal, test this. Do not forget to include the -I parameter to specify the path to web.mk:

    % make -I /home/nik/mywork
    for htmlfile in index.html about.html; do  /bin/cp -f $htmlfile 
      ../mystage/$htmlfile;  /bin/chmod 444 ../mystage/$htmlfile;  done
    for subdir in unix; do  (cd $subdir; bmake install);  done
    for htmlfile in index.html; do  /bin/cp -f $htmlfile /$htmlfile;
      /bin/chmod 444 /$htmlfile;  done
    cp: cannot create /index.html: Permission denied
    *** Error code 2
    
    Stop.
    *** Error code 1
    
    Stop.

More problems. This time the problem lies on the third line of output.

    ... /bin/cp -f $htmlfile /$htmlfile; ...

This is run by unix/Makefile. For some reason the install target is trying to install the file in to the root directory (/) instead of ../mystage/unix/.

There are actually three problems lurking in this code.

Recall that DESTDIR is used to specify the destination directory where files will be installed. DESTDIR is defined in the first Makefile, and not in unix/Makefile. The appropriate line from web.mk is ${CP} -f $$htmlfile ${DESTDIR}/$$htmlfile;. When DESTDIR is empty that becomes cp -f index.html /index.html, which is what the previous output shows. Unless you are running a very lax system, ordinary users will not be able to write to the root directory.

To solve this we must ensure that DESTDIR is defined before it is used.

We could add a definition for DESTDIR in unix/Makefile. This would work, but becomes one more line to maintain and change if you ever move these files from one directory to another. Far better to have make(1) do this for you.

Remember that variables can be defined in the Makefile or on the command line. The install target in web.mk can redefine DESTDIR in the for subdir ... loop.

Edit web.mk so that the appropriate line now looks like:

    (cd $$subdir; ${MAKE} DESTDIR=${DESTDIR}/$$subdir install);

In this way, the name of the subdirectory being processed is appended to the current value of DESTDIR.

That is the first of the three problems mentioned earlier fixed.

The second problem is the definition of DESTDIR in the first Makefile. It is currently ../mystage. That is fine for the top level directory, but will fail for any Makefiles in subdirectories because of the ... This is because as soon as the install target recurses down the directory tree .. will stop refering to the correct directory.

There are two ways this can be fixed. The approach you take depends upon how your website's staging area is organised.

The first approach works if you have one directory that will be used as the staging area and will never change. If this is the case then you can hard code the complete path to this directory in your top level Makefile. This works well if you are managing a large site with only one staging area, where you will always be able to write to this staging area regardless of which computer you are working on.

The second approach is similar to how the first problem was solved. However, instead of redefining DESTDIR on the make(1) command line by appending subdirectory names, this approach works by prepending ../ to DESTDIR on the command line. The new line would look like this:

    (cd $$subdir; ${MAKE} DESTDIR=../${DESTDIR}/$$subdir install);

This ensures that every time a new directory is entered an additional ../ is added so that the staging area can be found.

This approach works well if everyone can have their own private staging area where they can test their changes without causing problems for other people working on the site.

Both approaches have their merits, and you should choose which one works best for your site.

The third problem is a simple one. As soon as you try and install files in to directories you run the risk that the directory will not exist. install must make sure that DESTDIR exists before files are copied to it. Alter install in web.mk so that it starts like this:

    install:
            [ -d ${DESTDIR} ] || ${MKDIR} -p ${DESTDIR}
            for htmlfile in ${HTML}; do \
            ...

This demonstrates the or test, using the || connector. The || connector works by running the command(s) on the left of the connector, and if they fail, running the commands on the right. The above code checks for the existence of the DESTDIR directory. If the directory exists then the test succeeds, and nothing else happens. If the directory does not exist then the test fails, and the right hand side (${MKDIR} -p ${DESTDIR}) is run.

Note: This is really just a shorter way of writing this:

    if [ ! -d ${DESTDIR} ]; then \
            ${MKDIR} -p ${DESTDIR}; \
    fi

The above can be read as ``If a directory called ${DESTDIR} does not exist then make it''.

The end result is the same.

-p is for the case where you try and make a directory (/for/bar/baz) where one or more of the intermediate directories (bar) does not exist. Normally mkdir(1) will exit with an error if an intervening directory is missing. With -p mkdir(1) will make any missing intermediate directories as necessary.

This should not be required, because each intervening directory is made beforehand. However, it is better to be safe than sorry.

You will also need to add a definition for MKDIR to web.mk.

    MKDIR=  /bin/mkdir

Once more, test this:

    % make -I/home/nik/mywork
    [ -d ../mystage ] || /bin/mkdir -p ../mystage
    for htmlfile in index.html about.html; do  /bin/cp -f $htmlfile
      ../mystage/$htmlfile;  /bin/chmod 444 ../mystage/$htmlfile;  done
    for subdir in unix; do  (cd $subdir; make
      DESTDIR=../../mystage/$subdir install);  done
    [ -d ../../mystage/unix ] || /bin/mkdir -p ../../mystage/unix
    for htmlfile in index.html; do  /bin/cp -f $htmlfile
      ../../mystage/unix/$htmlfile;  /bin/chmod 444
      ../../mystage/unix/$htmlfile;  done

Note: In this example I elected to prepend ../ to DESTDIR in web.mk, rather than specifying a full path.

Success! The install target has successfully installed the files, recursed in to the unix subdirectory, made a unix subdirectory in the correct place in the staging area, and installed a file there.


Making more than one target recurse

install is not the only target that should recurse down through the directory structure, although it is the only one that we have written yet. A hypothetical clean (which removes generated files) or all (which would do many things, such as converting JPEG files to GIF files) should also recurse through the directories, running the same target.

As you should by now expect, duplicating the .if ... .endif code that you use in install in the clean and all targets would work, but is not the best approach---it makes the Makefiles less maintainable. What is required is a way to include the body of a rule inside another rule. Then you could create one rule which only contains the recursion code, and add this rule to the end of the other rules that you want to become recursive.

As you would expect, make(1) can do this. However, the approach you need to take is not as intuitive as it might be. To quote from the appropriate part of the make(1) manual page;

  .USE  Turn the target into make's version of a macro.  When the
        target is used as a source for another target, the other tar­
        get acquires the commands, sources, and attributes (except
        for .USE) of the source.  If the target already has commands,
        the .USE target's commands are appended to them.

That confused me the first few times I read it too. A simple example should clear this up. Create this simple Makefile somewhere;

    #
    # Demonstrating .USE
    #
    
    taregt-2: target-1
            @echo "This is target-2"
    
    target-1:  
            @echo "This is target-1"

Running it gives the following output;

    % make target-2
    This is target-1
    This is target-2

This is normal; target-2 depends on target-1, so target-1 runs first.

Now, introduce a special make(1) variable. You have already seen MAKE, which is the name of the make(1) command. .TARGET is another variable defined by make(1), and it contains the name of the target that make(1) is currently processing[2]. Change the body of those two rules;

    #
    # Demonstrating .USE
    #
    
    target-2: target-1
            @echo "This is ${.TARGET}"
    
    target-1:
            @echo "This is ${.TARGET}"

Then re-run the test.

    % make target-2
    This is target-1
    This is target-2

Again, this is all as expected. Now add .USE as a dependency for target-1.

    ⋮
    target-1: .USE
            @echo "This is ${.TARGET}"

Re-run the test once more;

    % make target-2
    This is target-2
    This is target-2

Remember how make(1) normally resolves dependencies. If it sees that any of the dependencies are out of date it postpones running the current target, and runs the target for each of the dependencies in turn.

If one of those dependencies (in this case, target-1) depends upon the special target .USE then make(1) behaves differently. The body of the dependency is appended to the body of the current target. You can easily confirm this by changing the text printed by target-1;

    ⋮
    target-1: .USE
            @echo "target-1:  This is ${.TARGET}"

Run this;

    % make
    This is target-2
    target-1:  This is target-2

As you have just confirmed, the body of target-1 is being run after the body of target-2 instead of before target-2 as normally happens with dependencies.

You should see how we can put the code that recurses in to subdirectories in to a target that depends on .USE. All the targets that we want to be able to recurse can then depend on this new target and they get the extra functionality without needing to write new code.

Go back to web.mk, alter install, and add a new target, like this;

    ⋮
    install: _SUBDIRUSE
            [ -d ${DESTDIR} ] || ${MKDIR} -p ${DESTDIR}
            @for htmlfile in ${HTML}; do \
                    ${CP} -f $$htmlfile ${DESTDIR}/$$htmlfile; \
                    ${CHMOD} 444 ${DESTDIR}/$$htmlfile; \
            done
          
    #
    # Any targets that depend on this one will be run recursively 
    # in all the subdirectories listed in ${SUBDIR}
    #
    _SUBDIRUSE: .USE
    .if defined(SUBDIR) && !empty(SUBDIR)
            @for subdir in ${SUBDIR}; do \
                    (cd $$subdir; ${MAKE} DESTDIR=${DESTDIR}/$$subdir \
                    ${.TARGET}); 
            done
    .endif

As you can see, all this has done is split the recursive code out of install, and in to its own target, called _SUBDIRUSE. install now lists this as a dependency. See how the body of this new target uses .TARGET instead of hardcoding the name of the target to call. This will allow _SUBDIRUSE to be used by other targets as well.

You now have a re-useable target that can be listed as a dependency for any other target, causing that target to be run in all the directories listed in SUBDIR.

You can prove this by creating a simple all target. Add the following to web.mk, and run make(1) calling the all target to confirm that it works.

    all: _SUBDIRUSE
            echo ${.TARGET}: in `pwd`

In the next article...

We have almost finished working through the most useful features of make(1). The next article in this series will explain how you can use make(1) to control the production of one file from another file (GIF files from JPEG files, for example). After that has been covered, the ins and outs of getting your website from the staging area on to the live site will be tackled.

In the mean time, use the techniques in this article to automatically copy your website from the work area to the staging area. Do not forget to cvs add and cvs commit your new Makefiles.

Notes

[1]

The backticks (or back-facing-single-quotes) surrounding the pwd(1) command are a feature of the shell. They mean ``Run the command inside the quotes, and replace the name of the command in the quotes with the output from the command''.

So if you are in /var/tmp and run echo I am in `pwd` then pwd(1) is run. pwd(1) prints the current directory name, which replaces the name of the command (and the quotes). The command line then becomes echo I am in /var/tmp, which is then run, and prints I am in /var/tmp.

[2]

A list of these variables is in the make(1) manual page, under the VARIABLE ASSIGNMENTS heading.

Nik Clayton, nik@freebsd.org