From e683f80bb5a847cc11251c45ff8b9553826b6eb5 Mon Sep 17 00:00:00 2001 From: Aki Date: Mon, 27 Mar 2023 22:36:21 +0200 Subject: Rewrote make introduction post --- how_to_build_software_with_make.html | 210 +++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 how_to_build_software_with_make.html (limited to 'how_to_build_software_with_make.html') diff --git a/how_to_build_software_with_make.html b/how_to_build_software_with_make.html new file mode 100644 index 0000000..dce05aa --- /dev/null +++ b/how_to_build_software_with_make.html @@ -0,0 +1,210 @@ + + + + + + + + + +How to Build Software With Make + + + +
+

How to Build Software With Make

+

Published on 2023-03-21 20:00:00+02:00 +

To compile an executable from a single C source file we need just one compiler invocation in our shell: +

+$ cc myprogram.c -o myprogram
+$ ./myprogram
+Hello, World!
+
+

As we expand our projects they naturally become harder to build. If not due to dependencies then due to pure +repetitiveness of the task. Build systems are a response to +that and GNU Make is one of them. GNU Make is part of a larger family +of programs that usually share the make(1) (or similar, e.g., nmake, mk). Although timeline doesn't necessarily +agree, we can say that make is defined in +POSIX standard. +

Principles of Make are straight-forward: as a user we define a set of rules that consist of targets, dependencies and +commands. Targets are usually filenames that describe what rule is expected to produce. Dependencies are names of other +targets or source files. That's it. Later this concept is expanded upon to reduce the amount of lines we need to write. +

When we build native software we are mainly interested in: +

+

Since make live close to the compiler we will be also interested in object files (e.g., *.o, *.obj). + +

How to Build Executables With Make

+

Make helps us to reduce number of characters we need to type in terminal and lines we need to put to setup the +build. In fact, the simplest case of one source file to one executable, like the one I showed above, does not need any +preparation at all: +

+$ make myprogram
+cc myprogram.c -o myprogram
+$ ./myprogram
+Hello, World!
+
+

First thing that we should observe is that make nonchalantly prints the commands it executes to build stuff +for us. I'll keep them visible in samples so it's hopefully more clear what's happening. +

We invoke make directly with a name of the target we intend to build. Remember: targets are usually filenames +that are expected to be produced by a rule. Based on the target name and built-in rules make guesses what to do +to get myprogram. It finds myprogram.c in the working directory and selects an appropriate rule. +

Yet, we still need to type the target name each time we want to build it. We can avoid it by creating a file called +Makefile in the project directory. This is the file containing all build rules and other helpful definitions. If +make is ran without any targets then the first target found in Makefile is used. Consider Makefile: +

+myprogram:
+
+

Let's try it: +

+$ make
+cc myprogram.c -o myprogram
+
+

Selects the first and the only target as intended.

+broom +

There are traditional targets +that users of our makefiles may expect. A less bare-bone makefile would look like this: +

+all: myprogram
+clean:
+	rm -f myprogram
+.PHONY: all clean
+
+

Now, someone can run following to ensure that a clean build is made: +

+$ make clean all
+rm -f myprogram
+cc myprogram.c -o myprogram
+
+

Let's note that we can specify multiple targets in the command line. They will be build in order. +

Next, all: myprogram - this notation specifies that target all depends on myprogram. We +can specify multiple dependencies if needed. All dependencies will be built and once they are done the dependant target +will be processed. +

.PHONY is a special target that is used to mark targets that are not expected to produce files. We are +not expecting all or clean files to be present at any stage. Instead we can imagine these as tasks or +actions. Other such targets would be for example install, uninstall or dist. + +

How to Build Object Files With Make

+

Usually, we won't implement our projects inside a single source file. We want to split logical parts of the software +into several files. Consider greeting.h: +

+#pragma once
+void greet(void);
+
+

With accompanying greeting.c: +

+#include <stdio.h>
+#include "greeting.h"
+void greet(void)
+{
+	printf("Hello, World!");
+}
+
+

And our program that uses them, let's stay with myprogram.c: +

+#include "greeting.h"
+int main()
+{
+	greet();
+}
+
+

As simple as it gets while it still illustrates the problem.

+files +

When we build our program from a single source the compiler does two things: compiles and links. Now we want to do +these separately. An intermediate result of compilation is an object file. On Linux and Unix platforms they usually use +*.o extension. We can instruct make to use them by adding new rules to our existing makefile: +

+myprogram: myprogram.o greeting.o
+myprogram.o: greeting.h
+greeting.o: greeting.h
+
+

First we declare that in order to build myprogram two object files are needed. Then we define some additional +relationships between the object files and the only header file that we have. +

Note that we did not specify explicitly dependency between the object files and the respective source files. +Technically we could also skip the relationship between myprogram and myprogram.o and make would +still figure it out. This is a matter of what we want to declare explicitly, how lazy we are at the time of writing and +how permissive are the built-in rules that we use. +

Let's try it out: +

+$ make
+cc -c -o myprogram.o myprogram.c
+cc -c -o greeting.o greeting.c
+cc myprogram.o greeting.o -o myprogram
+$ ./myprogram
+Hello, World!
+
+ +

How to Build Static Libraries With Make

+

The best part here is that we're pretty much already set. Static libraries are archives containing object files. They +are created and updates using ar(1). There are built-in rules available for them, too, but with one additional +gimmick. They use archive targets that look like this: +

+myprogram: myprogram.o libgreeting.a
+libgreeting.a: libgreeting.a(greeting.o)
+
+

We replaced greeting.o prerequisite with libgreeting.a which is defined line below. Dependencies of the +library target are expressed as +archive members. + +

How to Build Shared Libraries With Make

+

Shared/dynamic libraries are different. The process of building them with make is similar to executables, but +we want to modify compilation and linking options, and write our own command: +

+CFLAGS=-fPIC
+LDFLAGS=-shared
+libgreeting.so: greeting.o
+	$(LD) -shared $(LDLIBS) $(LDFLAGS) $^ -o $@
+
+

First two lines are variable definitions (called "macros" in POSIX). We set CFLAGS - C compiler flags - to +-fPIC to enable position-independent +code, which is needed when compiling code for shared libraries that are expected to work nicely on Linux. Then, we +set LDFLAGS - linker flags - to -shared which (put simply) tells the linker to build a shared +object. +

Now let's analyse linker command itself. Starting with $(LD) which is substituted with linker +executable. We already know $(LDFLAGS), so $(LDLIBS) sounds like "linker libraries" and that's +correct. Finally, we have two +automatic variables also known +as internal +macros: $^ (substituted by all dependencies) and $@ (substituted by target name). Note +that $^ is not part of POSIX. +

When we finally run make we will see how it performs all predicted substitutions: +

+$ make
+cc -fPIC -c -o greeting.o greeting.c
+ld -shared greeting.o -o libgreeting.so
+
+

Value for $(LD) is provided by default by make. +

In some cases, we may want to invoke the linker via the compiler to have all defaults implicitly defined. For example +for C++ we would replace $(LD) with $(CXX) to imply e.g., -lstdc++. + +

How to Build Against External Libraries With Make

+

This one starts to be tricky. The simple case where we use libraries installed in /usr or /usr/local is +exactly that - simple. All we need to do is put linker options in a variable (macro) that we have already seen: +$(LDLIBS): +

+LDLIBS=-llua -lm
+
+

Since managing these manually easily becomes a problem there are tools that can generate list of these options for +us. One of them is pkgconfig(1). In most make implementations we can use backticks to run command and use +its output: +

+CFLAGS=`pkg-config --cflags lua`
+LDLIBS=`pkg-config --libs lua`
+
+

This will link Lua and make its headers available. + +

What's next?

+

This is only a beginning. I hope it's a good overview, but the true journey awaits. This is but a dump of information +and playing around with make sounds like a reasonable next step. Because of its simple design make is not +limited to C, C++, yacc/bison or Fortran. I use it for most small weekend projects I do, be it RST to HTML generation or +image rendering. Anything that can be described as processing from one file to another (or generation from thin air) is +good enough to be put into a makefile. +

Next step after playing around some is diving deeper into automatic variables and +pattern rules. I'd recommend to +skip over inference +rules/suffixes. +

+ -- cgit v1.1