← Back
Script House — Linux Admin

Chapter 12: Compiling Source Code Lab

Hands-on practice with ./configure, make, gcc, and dependency management

Lab Overview

Compiling from source is a foundational sysadmin skill — required when packages are unavailable, outdated, or need custom compile-time options. This lab walks through the complete build pipeline: installing build tools, writing and compiling C programs directly with gcc, working with a Makefile project, and using ./configure with custom installation prefixes.

1

Build Environment Setup

Install essential build tools and verify the toolchain

BEGINNER
The Build Toolchain Before you can compile anything, you need the compiler (gcc), the build automation tool (make), the standard C library headers (libc6-dev), and optionally autoconf for projects using the configure script system. On Debian/Ubuntu, build-essential installs all the core tools in one package.
1

Install the build toolchain (skip if already installed):

sudo apt update -qq sudo apt install -y build-essential autoconf automake pkg-config echo "Build tools installed."
Expected output
Reading package lists... Done Building dependency tree... Done ... build-essential is already the newest version (12.9ubuntu3). autoconf is already the newest version (2.71-2). ...
2

Verify each tool is present and note the version:

gcc --version | head -1 make --version | head -1 autoconf --version | head -1 pkg-config --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 GNU Make 4.3 autoconf (GNU Autoconf) 2.71 1.8.0
3

Check how many CPU cores are available for parallel builds:

nproc echo "You can use make -j$(nproc) to parallelize builds"
4 You can use make -j4 to parallelize builds
4

Create the lab workspace:

mkdir -p ~/lab-compile/{hello-c,makefile-project,autoconf-project,prefix-install} ls ~/lab-compile/
autoconf-project hello-c makefile-project prefix-install
2

Compiling C Programs with gcc

Write and compile C source files directly — single file and multi-file projects

BEGINNER
1

Write a minimal C program. Use cat with a heredoc to create the source file:

cat > ~/lab-compile/hello-c/hello.c <<'EOF' #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { printf("Hello from compiled C!\n"); printf("Arguments received: %d\n", argc - 1); for (int i = 1; i < argc; i++) { printf(" arg[%d]: %s\n", i, argv[i]); } return EXIT_SUCCESS; } EOF cat ~/lab-compile/hello-c/hello.c
Source file content
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { printf("Hello from compiled C!\n"); printf("Arguments received: %d\n", argc - 1); ... }
2

Compile to an executable with gcc. The -o flag specifies the output filename:

gcc ~/lab-compile/hello-c/hello.c -o ~/lab-compile/hello-c/hello ls -lh ~/lab-compile/hello-c/hello
-rwxrwxr-x 1 user user 16K Jan 15 09:20 /home/user/lab-compile/hello-c/hello

The binary is executable (x permission). It is 16KB because it includes the C standard library linked in.

3

Run the compiled program:

~/lab-compile/hello-c/hello ~/lab-compile/hello-c/hello alpha beta gamma
Hello from compiled C! Arguments received: 0 Hello from compiled C! Arguments received: 3 arg[1]: alpha arg[2]: beta arg[3]: gamma
4

Compile with debugging symbols and warnings enabled (good development practice):

gcc -g -Wall -Wextra -o ~/lab-compile/hello-c/hello-debug \ ~/lab-compile/hello-c/hello.c ls -lh ~/lab-compile/hello-c/
-rwxrwxr-x 1 user user 16K Jan 15 09:20 hello -rwxrwxr-x 1 user user 28K Jan 15 09:20 hello-debug

The debug binary is larger due to embedded symbol information. The -Wall and -Wextra flags catch potential bugs at compile time.

5

Compile with optimization for a production binary (smaller and faster):

gcc -O2 -o ~/lab-compile/hello-c/hello-opt ~/lab-compile/hello-c/hello.c ls -lh ~/lab-compile/hello-c/hello*
-rwxrwxr-x 1 user user 16K hello -rwxrwxr-x 1 user user 28K hello-debug -rwxrwxr-x 1 user user 16K hello-opt
6

Inspect what shared libraries the binary depends on:

ldd ~/lab-compile/hello-c/hello
linux-vdso.so.1 (0x00007fffd7ff5000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a1b200000) /lib64/ld-linux-x86-64.so.2 (0x00007f8a1b3f2000)

This binary dynamically links to libc.so.6 — the C standard library. If this library is missing, the binary will not run.

3

Writing and Using Makefiles

Create a multi-file project with a Makefile for automated builds and cleaning

INTERMEDIATE
Why Make? For projects with multiple source files, manually running gcc on each file becomes error-prone. make reads a Makefile that describes build rules, dependencies, and targets. It only recompiles files that have changed — a critical optimization for large codebases.
1

Create a simple multi-file C project. First, a utility header and implementation:

cd ~/lab-compile/makefile-project # Header file cat > utils.h <<'EOF' #ifndef UTILS_H #define UTILS_H void print_banner(const char *title); int add_numbers(int a, int b); #endif EOF # Implementation file cat > utils.c <<'EOF' #include <stdio.h> #include "utils.h" void print_banner(const char *title) { printf("=== %s ===\n", title); } int add_numbers(int a, int b) { return a + b; } EOF # Main program cat > main.c <<'EOF' #include <stdio.h> #include "utils.h" int main(void) { print_banner("Makefile Project Demo"); int result = add_numbers(42, 58); printf("42 + 58 = %d\n", result); return 0; } EOF ls
main.c utils.c utils.h
2

Create the Makefile. Note: Makefiles require TABS (not spaces) before commands:

cat > Makefile <<'EOF' # Compiler and flags CC = gcc CFLAGS = -Wall -Wextra -O2 TARGET = myapp # Object files (one per source file) OBJS = main.o utils.o # Default target: build the executable all: $(TARGET) # Link object files into executable $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $(TARGET) $(OBJS) @echo "Build complete: $(TARGET)" # Compile main.c to main.o main.o: main.c utils.h $(CC) $(CFLAGS) -c main.c # Compile utils.c to utils.o utils.o: utils.c utils.h $(CC) $(CFLAGS) -c utils.c # Remove compiled files clean: rm -f $(OBJS) $(TARGET) @echo "Cleaned build files." # Install to /usr/local/bin install: $(TARGET) install -m 755 $(TARGET) /usr/local/bin/ @echo "Installed $(TARGET) to /usr/local/bin/" .PHONY: all clean install EOF cat Makefile
CC = gcc CFLAGS = -Wall -Wextra -O2 TARGET = myapp OBJS = main.o utils.o ... .PHONY: all clean install
3

Build the project using make:

make
gcc -Wall -Wextra -O2 -c main.c gcc -Wall -Wextra -O2 -c utils.c gcc -Wall -Wextra -O2 -o myapp main.o utils.o Build complete: myapp
ls -lh ./myapp
-rw-rw-r-- 1 user user 252 Makefile -rwxrwxr-x 1 user user 17K myapp -rw-rw-r-- 1 user user 1.7K main.c main.o utils.c utils.h utils.o === Makefile Project Demo === 42 + 58 = 100
4

Run make again to see incremental build detection (nothing recompiles):

make
make: 'myapp' is up to date.

Make checks timestamps. Since no source file is newer than the target, it skips compilation. This saves enormous time on large projects.

5

Modify a source file and observe that only the affected file recompiles:

touch utils.c # Simulate a file edit (updates timestamp) make
gcc -Wall -Wextra -O2 -c utils.c gcc -Wall -Wextra -O2 -o myapp main.o utils.o Build complete: myapp

Only utils.c was recompiled. main.o was reused from the previous build.

6

Use make clean to remove all build artifacts:

make clean ls
Cleaned build files. main.c Makefile utils.c utils.h
Common Makefile Targets
TargetPurposeNotes
make / make allBuild the default targetDefined first in Makefile
make cleanRemove .o files and binariesKeeps source files
make distcleanRemove everything including configure outputReturns to pristine state
make installCopy binary to system pathsUsually requires sudo
make uninstallRemove installed filesNot all projects implement this
make check / testRun the test suiteProject-dependent
make -j$(nproc)Parallel build using all coresDramatically speeds large projects
4

The configure / make / make install Pipeline

Work through the full autoconf build pipeline with a custom install prefix

INTERMEDIATE
The Three-Step Build Most open source projects using autoconf follow this exact sequence:
./configure
make
make install

configure probes the system and generates a Makefile. make compiles. make install deploys the binary to the prefix path.

1

Create a minimal autoconf project to simulate a real-world source download:

cd ~/lab-compile/autoconf-project # Main source file cat > myutil.c <<'EOF' #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { if (argc < 2) { printf("Usage: myutil <message>\n"); return 1; } int len = strlen(argv[1]); printf("Message: %s\n", argv[1]); printf("Length: %d characters\n", len); return 0; } EOF
2

Create a configure.ac file (the input to autoconf) and Makefile.am (the input to automake):

cat > configure.ac <<'EOF' AC_INIT([myutil], [1.0], [admin@lab.local]) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_PROG_CC AC_CONFIG_FILES([Makefile]) AC_OUTPUT EOF cat > Makefile.am <<'EOF' bin_PROGRAMS = myutil myutil_SOURCES = myutil.c EOF
3

Generate the configure script from configure.ac using autoreconf:

autoreconf --install 2>&1 | tail -5
configure.ac:2: installing './compile' configure.ac:2: installing './install-sh' configure.ac:2: installing './missing' Makefile.am: installing './depcomp'
ls configure file configure
configure configure: POSIX shell script, ASCII text executable
4

Run ./configure with a custom --prefix to install into ~/local instead of /usr/local. This avoids needing root permissions:

mkdir -p ~/local ./configure --prefix=$HOME/local 2>&1 | tail -8
checking for gcc... gcc checking whether the C compiler works... yes checking for C compiler default output file name... a.out checking for suffix of executables... checking whether we are cross compiling... no checking for suffix of object files... o checking whether we are using the GNU C compiler... yes configure: creating ./config.status
ls Makefile # Generated by configure
Makefile
5

Compile the project using parallel make:

make -j$(nproc) 2>&1 | tail -5
gcc -DPACKAGE_NAME=\"myutil\" -DPACKAGE_VERSION=\"1.0\" ... gcc -g -O2 -o myutil myutil-myutil.o make[1]: Leaving directory '/home/user/lab-compile/autoconf-project'
6

Install to the custom prefix and test the installed binary:

make install 2>&1 | grep "installing" ls -lh ~/local/bin/myutil ~/local/bin/myutil "Linux compilation is powerful"
/usr/bin/install -c myutil '/home/user/local/bin/myutil' -rwxr-xr-x 1 user user 18K Jan 15 09:25 /home/user/local/bin/myutil Message: Linux compilation is powerful Length: 28 characters
5

Diagnosing and Fixing Build Errors

Identify and resolve the three most common compile failures

ADVANCED
Why This Matters Most source compilation failures fall into three categories: missing development headers, missing libraries, or gcc version mismatches. Learning to read error messages and diagnose the root cause is the real skill — once you can read compiler output, any build problem becomes tractable.
1

Error Type 1: Missing Header File — The most common configure failure

configure: error: Package requirements (libssl) were not met: No package 'libssl' found Consider adjusting the PKG_CONFIG_PATH environment variable if you installed software in a non-standard prefix.
Diagnosis: The configure script cannot find the OpenSSL development headers.
Fix: Install the -dev package that provides the headers.
# Find which package provides the missing header apt-file search openssl/ssl.h 2>/dev/null | head -3 # Or simply search by package name pattern: apt-cache search "libssl.*dev" # Fix: install the dev package sudo apt install -y libssl-dev # Then re-run configure
libssl-dev - Secure Sockets Layer toolkit - development files
2

Error Type 2: Linker Error (undefined reference)

main.c:(.text+0x1a): undefined reference to `sqrt' collect2: error: ld returned 1 exit status
Diagnosis: The math library function sqrt() was used in source code but the math library was not linked.
Fix: Add -lm to the gcc command to link libm.
# Wrong (produces the error above): gcc -o myapp myapp.c # Correct (links the math library): gcc -o myapp myapp.c -lm # For configure-based projects, set LDFLAGS: ./configure LDFLAGS="-lm"
3

Error Type 3: Library Not Found at Runtime (after successful compilation)

./myapp: error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory
Diagnosis: The binary compiled successfully but the shared library is not in the dynamic linker's search path at runtime.
Fix: Run ldconfig to refresh the linker cache, or set LD_LIBRARY_PATH.
# Step 1: Find where the library actually is ldconfig -p | grep libfoo find /usr /lib /opt -name "libfoo*.so*" 2>/dev/null # Step 2: If installed to /usr/local/lib, refresh cache sudo ldconfig ldconfig -p | grep libfoo # Step 3: Temporary fix (not recommended for production) export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH ./myapp
4

Use pkg-config to automatically get the correct flags for installed libraries:

# Ask pkg-config what flags to use for OpenSSL pkg-config --cflags openssl 2>/dev/null || echo "openssl not found by pkg-config" pkg-config --libs openssl 2>/dev/null || echo "(try: sudo apt install libssl-dev)" # Use pkg-config output directly in gcc: gcc -o ssl-app ssl-app.c $(pkg-config --cflags --libs openssl)
-I/usr/include/openssl -lssl -lcrypto
5

Inspect a compiled binary to understand its shared library dependencies:

ldd ~/lab-compile/autoconf-project/myutil readelf -d ~/lab-compile/autoconf-project/myutil | grep NEEDED
linux-vdso.so.1 (0x00007fffd7ff5000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...) /lib64/ld-linux-x86-64.so.2 (0x00007f...) 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
The checkinstall Alternative Instead of running sudo make install (which installs files invisibly with no package manager tracking), use checkinstall to wrap the installation into a .deb package. This lets you track, upgrade, or remove the custom-compiled software cleanly: sudo checkinstall --pkgname=myutil --pkgversion=1.0. Install it with: sudo apt install checkinstall.

Lab Complete

You set up the build toolchain, compiled C programs directly with gcc, wrote and used a Makefile for a multi-file project, ran the full configure/make/make install pipeline with a custom prefix, and diagnosed the three most common build errors.

This lab is already marked complete.