Installing Products

We've reached the point where our users' experiences with Jupiter should be fairly painless—even pleasant—as far as building the project is concerned. Users will simply unpack the distribution tarball, change into the distribution directory, and type make. It really can't get any simpler than that.

But we still lack one important feature—installation. In the case of the Jupiter project, this is fairly trivial. There's only one program, and most users would guess correctly that to install it, they should copy jupiter into either their /usr/bin or /usr/local/bin directory. More complex projects, however, could cause users some real consternation when it comes to where to put user and system binaries, libraries, header files, and documentation including man pages, info pages, PDF files, and the more-or-less obligatory README, AUTHORS, NEWS, INSTALL, and COPYING files generally associated with GNU projects.

We don't really want our users to have to figure all that out, so we'll create an install target to manage putting things where they go once they're built properly. In fact, why not just make installation part of the all target? Well, let's not get carried away. There are actually a few good reasons for not doing this.

First, build and installation are separate logical concepts. The second reason is a matter of filesystem rights. Users have rights to build projects in their own home directories, but installation often requires root-level rights to copy files into system directories. Finally, there are several reasons why a user may wish to build but not install a project, so it would be unwise to tie these actions together.

While creating a distribution package may not be an inherently recursive process, installation certainly is, so we'll allow each subdirectory in our project to manage installation of its own components. To do this, we need to modify both the top-level and the src-level makefiles. Changing the top-level makefile is easy: Since there are no products to be installed in the top-level directory, we'll just pass the responsibility on to src/Makefile in the usual way.

The modifications for adding an install target are shown in Example 2-18 and Example 2-19.

Example 2-18. Makefile: Passing the install target to src/Makefile

...
all clean check install jupiter:
        cd src && $(MAKE) $@
...
.PHONY: FORCE all clean check dist distcheck install

Example 2-19. src/Makefile: Implementing the install target

...
install:
        cp jupiter /usr/bin
        chown root:root /usr/bin/jupiter
        chmod +x /usr/bin/jupiter

.PHONY: all clean check install

In the top-level makefile shown in Example 2-18, I've added install to the list of targets passed down to src/Makefile. The installation of files is actually handled by the src-level makefile shown in Example 2-19.

Installation is a bit more complex than simply copying files. If a file is placed in the /usr/bin directory, then root should own it, so that only root can delete or modify it. Additionally, the jupiter binary should be flagged executable, so I've used the chmod command to set the mode of the file as such. This is probably redundant, as the linker ensures that jupiter is created as an executable file, but some types of executable products are not generated by a linker—shell scripts, for example.

Now our users can just type the following sequence of commands and the Jupiter project will be built, tested, and installed with the correct system attributes and ownership on their platforms:

$ gzip -cd jupiter-1.0.tar.gz | tar xf -
$ cd jupiter-1.0
$ make all check
...
$ sudo make install
Password: ******
...

All of this is well and good, but it could be a bit more flexible with regard to where things are installed. Some users may be okay with having jupiter installed into the /usr/bin directory. Others are going to ask why it isn't installed into the /usr/local/bin directory—after all, this is a common convention. We could change the target directory to /usr/local/bin, but then users may ask why they don't have the option of installing into their home directories. This is the perfect situation for a little command-line supported flexibility.

Another problem with our current build system is that we have to do a lot of stuff just to install files. Most Unix systems provide a system-level program—usually a shell script—called install that allows a user to specify various attributes of the files being installed. The proper use of this utility could simplify things a bit for Jupiter's installation, so while we're adding location flexibility, we might as well use the install utility, too. These modifications are shown in Example 2-20 and Example 2-21.

Notice that I only declared and assigned the prefix variable in the top-level makefile, but I referenced it in src/Makefile. I can do this because I used the export modifier at ❶ in the top-level makefile—this modifier exports the make variable to the shell that make spawns when it executes itself in the src directory. This feature of make allows us to define all of our user variables in one obvious location—at the beginning of the top-level makefile.

I've now declared the prefix variable to be /usr/local, which is very nice for those who want to install jupiter in /usr/local/bin, but not so nice for those who want it in /usr/bin. Fortunately, make allows you to define make variables on the command line, in this manner:

$ sudo make prefix=/usr install
...

Remember that variables defined on the command line override those defined within the makefile.[31] Thus, users who want to install jupiter into the /usr/bin directory now have the option of specifying this on the make command line.

With this system in place, our users may install jupiter into a bin directory beneath any directory they choose, including a location in their home directory (for which they do not need additional rights). This is, in fact, the reason we added the install -d $(prefix)/bin command at ❷ in Example 2-21—this command creates the installation directory if it doesn't already exist. Since we allow the user to define prefix on the make command line, we don't actually know where the user is going to install jupiter; therefore, we have to be prepared for the possibility that the location may not yet exist. Give this a try:

$ make all
$ make prefix=$PWD/_inst install
$
$ ls −1p
_inst/
Makefile
src/
$
$ ls −1p _inst
bin/
$
$ ls −1p _inst/bin
jupiter
$

What if a user doesn't like our package after he's installed it, and he just wants to get it off his system? This is a fairly likely scenario for the Jupiter project, as it's rather useless and takes up valuable space in his bin directory. In the case of your projects, however, it's more likely that a user would want to do a clean install of a newer version of the project or replace the test build he downloaded from the project website with a professionally packaged version that comes with his Linux distribution. Support for an uninstall target would be very helpful in situations like these.

Example 2-22 and Example 2-23 show the addition of an uninstall target to our two makefiles.

As with the install target, this target requires root-level rights if the user is using a system prefix, such as /usr or /usr/local. You should be very careful about how you write your uninstall targets; unless a directory belongs specifically to your package, you shouldn't assume you created it. If you do, you may end up deleting a system directory like /usr/bin!

The list of things to maintain in our build system is getting out of hand. There are now two places we need to update when we change our installation processes: the install and uninstall targets. Unfortunately, this is really about the best we can hope for when writing our own makefiles, unless we resort to fairly complex shell script commands. But hang in there—in Chapter 5 I'll show you how to rewrite this makefile in a much simpler way using GNU Automake.

Now let's add some code to our distcheck target to test the functionality of the install and uninstall targets. After all, it's fairly important that both of these targets work correctly from our distribution tarballs, so we should test them in distcheck before declaring the tarball release-worthy. Example 2-24 illustrates the necessary changes to the top-level makefile.

Note that I used a double dollar sign on the $${PWD} variable references, ensuring that make passes the variable reference to the shell with the rest of the command line, rather than expanding it inline before executing the command. I wanted this variable to be dereferenced by the shell, rather than the make utility.[32]

What we're doing here is testing to ensure the install and uninstall targets don't generate errors—but this isn't very likely because all they do is install files into a temporary directory within the build directory. We could add some code immediately after the make install command that looks for the products that are supposed to be installed, but that's more than I'm willing to do. One reaches a point of diminishing returns, where the code that does the checking is just as complex as the installation code—in which case the check becomes pointless.

But there is something else we can do: We can write a more or less generic test that checks to see if everything we installed was properly removed. Since the stage directory was empty before our installation, it had better be in a similar state after we uninstall. Example 2-25 shows the addition of this test.

The test first generates a numeric value at ❶ in a shell variable called remaining, which represents the number of regular files found in the stage directory we used. If this number is not zero, it prints a message to the console at ❷ indicating how many files were left behind by the uninstall commands and then it exits with an error. Exiting early leaves the stage directory intact so we can examine it to find out which files we forgot to uninstall.

I don't want to alarm people by printing the embedded echo statement unless it really should be executed, so I prefixed the entire test with an at sign (@) so that make wouldn't print the code to stdout. Since make considers these five lines of code to be a single command, the only way to suppress printing the echo statement is to suppress printing the entire command.

Now, this test isn't perfect—not by a long shot. This code only checks for regular files. If your installation procedure creates any soft links, this test won't notice if they're left behind. The directory structure that's built during installation is purposely left in place because the check code doesn't know whether a subdirectory within the stage directory belongs to the system or to the project. The uninstall rule's commands can be aware of which directories are project specific and properly remove them, but I don't want to add project-specific knowledge into the distcheck tests—it's that problem of diminishing returns again.



[31] Unfortunately, some make implementations do not propagate such command-line variables to recursive $(MAKE) processes. To alleviate this potential problem, variables that might be set on the command line can be passed as var="$(var)" on sub-make command lines. My simple examples ignore this issue because it's a corner case, but you should at least be aware of this problem.

[32] Technically, I didn't have to do this because the PWD make variable was initialized from the environment, but it serves as a good example of this process. Additionally, there are corner cases where the PWD make variable is not quite as accurate as the PWD shell variable. It may be left pointing to the parent directory on a subdirectory make invocation.