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 checkinstall
jupiter: cd src && $(MAKE) $@ ... .PHONY: FORCE all clean check dist distcheckinstall
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 checkinstall
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.
Example 2-20. Makefile: Adding a prefix
variable
...prefix=/usr/local
❶export prefix
all clean check install jupiter: cd src && $(MAKE) $@ ...
Example 2-21. src/Makefile: Using the prefix
variable in the install
target
... install: ❷install -d $(prefix)
/bininstall -m 0755 jupiter $(prefix)
/bin ...
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.
GNU make
allows you to use the export
keyword on the assignment line, but this syntax is not portable between GNU make
and other versions of make
.
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.
Example 2-22. Makefile: Adding the uninstall
target to the top-level makefile
... all clean installuninstall
jupiter: cd src && $(MAKE) $@ ... .PHONY: FORCE all clean dist distcheck installuninstall
Example 2-23. src/Makefile: Adding the uninstall
target to the src-level makefile
...uninstall:
-rm $(prefix)/bin/jupiter
.PHONY: all clean check installuninstall
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.
Example 2-24. Makefile: Adding distcheck
tests for the install
and uninstall
targets
... distcheck: $(distdir).tar.gz gzip -cd $(distdir).tar.gz | tar xvf - cd $(distdir) && $(MAKE) all cd $(distdir) && $(MAKE) checkcd $(distdir) && $(MAKE) prefix=$${PWD}/_inst install
cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst uninstall
cd $(distdir) && $(MAKE) clean
rm -rf $(distdir) @echo "*** Package $(distdir).tar.gz is ready for distribution." ...
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.
Example 2-25. Makefile: Adding a test for leftover files after uninstall
finishes
...
distcheck: $(distdir).tar.gz
gzip -cd $(distdir).tar.gz | tar xvf -
cd $(distdir) && $(MAKE) all
cd $(distdir) && $(MAKE) check
cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst install
cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst uninstall
❶ @remaining="`find $${PWD}/$(distdir)/_inst -type f | wc -l`"; \
if test "$${remaining}" -ne 0; then \
echo "*** $${remaining} file(s) remaining in stage directory!"; \
exit 1; \
fi
cd $(distdir) && $(MAKE) clean
rm -rf $(distdir)
❷ @echo "*** Package $(distdir).tar.gz is ready for distribution."
...
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.
This test code represents a good use of multiple shell commands passed to a single shell. I had to do this here so that the value of remaining
would be available for use by the if
statement. Conditionals don't work very well when the closing fi
is not executed by the same shell as the opening if
!
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.