Building from Source: A Complete Guide to make and gcc

Ever wondered how the software on your Linux system actually gets built? While package managers like apt and dnf make installing software incredibly easy, there are times when you need to roll up your sleeves and build software directly from its source code. Maybe you need a newer version that’s not in your distribution’s repositories, or perhaps you want to customize the build with specific options. Whatever the reason, understanding how to compile software from source is a fundamental skill every Linux user should have.

In this guide, we’re going to walk through the entire process of building software from source code, using the traditional Unix tools make and gcc. Don’t worry if you’re new to this – I’ll explain everything step by step, from what these tools actually do to handling common problems you might encounter.

What Does “Building from Source” Actually Mean?

When developers write software, they write it in human-readable programming languages like C, C++, or others. This source code is like a recipe written in English – it makes sense to humans, but computers can’t execute it directly.

Building from source means taking this human-readable code and transforming it into machine code that your computer can actually run. It’s like translating that English recipe into a language your computer understands.

Here’s what typically happens:

  1. Source code (written by developers) gets processed by a compiler
  2. The compiler (like gcc) translates it into machine code
  3. A build system (like make) orchestrates this process for complex projects
  4. You end up with an executable program you can run

Meet Your Tools: gcc and make

Before we dive in, let’s understand our main tools:

gcc (GNU Compiler Collection): This is your translator. It takes source code written in languages like C or C++ and converts it into executable machine code. Think of it as a very smart translator that understands programming languages.

make: This is your project manager. Real software projects have dozens or hundreds of source files, and make figures out which files need to be compiled, in what order, and how to link them all together. It reads instructions from a file called Makefile.

Step 1: Setting Up Your Build Environment

Before you can build anything, you need the right tools installed. Most Linux distributions don’t include development tools by default to keep the base system lean.

On Ubuntu/Debian systems:

1
2
sudo apt update
sudo apt install build-essential

Let me break this down:

  • sudo: We need administrator privileges to install system packages
  • apt update: This refreshes the list of available packages from repositories
  • apt install build-essential: This installs a bundle that includes gcc, make, and other essential build tools

On CentOS/RHEL/Fedora systems:

1
sudo dnf groupinstall "Development Tools"

The “Development Tools” group is similar to Ubuntu’s build-essential – it’s a collection of everything you need to compile software.

Verify your installation:

1
2
gcc --version
make --version

You should see version information for both tools. If you get “command not found” errors, the installation didn’t work correctly.

Step 2: Understanding a Simple Example

Let’s start with something simple before tackling real-world software. I’ll show you how to compile a basic C program to illustrate the concepts.

Create a simple C program:

1
2
mkdir ~/build-example
cd ~/build-example

Here we’re creating a dedicated directory for our example. It’s good practice to keep build experiments contained.

Now create a simple C file:

1
2
3
4
5
6
7
8
cat > hello.c << 'EOF'
#include <stdio.h>

int main() {
    printf("Hello, I was built from source!\n");
    return 0;
}
EOF

This command creates a file called hello.c with our simple C program. The << 'EOF' syntax is called a “here document” – it lets us type multiple lines of content directly into the file.

Now let’s compile it manually with gcc:

1
gcc hello.c -o hello

Breaking this down:

  • gcc: Our compiler
  • hello.c: The source file we want to compile
  • -o hello: The -o flag tells gcc what to name the output file (the compiled program)

Run your compiled program:

1
./hello

The ./ tells your shell to run the program in the current directory. You should see “Hello, I was built from source!” printed to your terminal.

Step 3: The Traditional Build Process (Configure, Make, Make Install)

Most open-source software follows a standard three-step build process that’s been used for decades:

  1. Configure: Set up the build for your specific system
  2. Make: Actually compile the software
  3. Make Install: Copy the compiled software to the right places on your system

Let’s walk through this with a real example. We’ll build a simple but useful program called tree, which displays directory structures in a nice tree format.

Download the source code:

1
2
cd ~
wget http://mama.indstate.edu/users/ice/tree/src/tree-2.1.1.tgz

This downloads the source code archive. Most source code is distributed as compressed archives (.tar.gz or .tgz files).

Extract the archive:

1
2
tar -xzf tree-2.1.1.tgz
cd tree-2.1.1

Let’s break down that tar command:

  • tar: The archive tool
  • -x: Extract files from an archive
  • -z: Handle gzip compression (the ‘z’ in .tgz)
  • -f: Specify the filename to work with

Examine what we have:

1
ls -la

You’ll typically see files like:

  • Makefile: Instructions for how to build the program
  • *.c files: The actual source code
  • README or INSTALL: Documentation about building and installing
  • Sometimes a configure script

Read the documentation:

1
cat README

Always read the README first! It contains important information about dependencies, build options, and installation instructions specific to that software.

Build the software:

1
make

This command tells make to read the Makefile and start building. You’ll see output like:

1
2
3
4
gcc -O2 -Wall -fomit-frame-pointer -c -o tree.o tree.c
gcc -O2 -Wall -fomit-frame-pointer -c -o unix.o unix.c
gcc -O2 -Wall -fomit-frame-pointer -c -o html.o html.c
...

Each line shows gcc compiling individual source files into object files (.o files), then linking them together into the final executable.

Test the compiled program:

1
./tree

This should show you a tree view of the current directory. The program works, but it’s only in our build directory.

Install the software system-wide:

1
sudo make install

This copies the compiled program to standard system directories (usually /usr/local/bin) so you can run it from anywhere. You need sudo because you’re writing to system directories.

Now you can run tree from anywhere:

1
tree /etc

Step 4: The Configure Script Approach

Many projects use a configure script before the make step. This script examines your system and creates a customized Makefile. Let’s see this in action with a different project.

1
2
3
4
cd ~
wget https://ftp.gnu.org/gnu/hello/hello-2.12.tar.gz
tar -xzf hello-2.12.tar.gz
cd hello-2.12

List the contents:

1
ls

You’ll see a configure script. This is a shell script that checks your system’s capabilities and creates a Makefile tailored to your environment.

Run the configure script:

1
./configure

You’ll see lots of output like:

1
2
3
4
5
6
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
...

The configure script is checking:

  • What compiler you have and its capabilities
  • What libraries are available on your system
  • Where system files are located
  • What features to enable or disable

Examine the generated Makefile:

1
ls -la Makefile

The configure script created this Makefile based on what it discovered about your system.

Build and install:

1
2
make
sudo make install

Step 5: Handling Dependencies

Real-world software often depends on libraries or other software components. When these are missing, your build will fail with errors. Let’s look at how to handle this.

Common dependency errors:

If you see errors like:

1
fatal error: someheader.h: No such file or directory

This means the build process can’t find a required header file, which usually indicates a missing development library.

Finding and installing dependencies:

On Ubuntu/Debian:

1
2
3
4
5
# Search for development packages
apt search libname-dev

# Install a development package
sudo apt install libssl-dev

Development packages (ending in -dev) contain header files needed for compilation.

On CentOS/RHEL/Fedora:

1
2
3
4
5
# Search for development packages
dnf search libname-devel

# Install a development package
sudo dnf install openssl-devel

On Red Hat-based systems, development packages end in -devel.

Example: Building software that needs SSL support

Let’s say you’re building software that needs SSL encryption capabilities:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# This would fail without SSL development libraries
./configure

# If you see SSL-related errors, install the development package:
sudo apt install libssl-dev  # On Ubuntu/Debian
# or
sudo dnf install openssl-devel  # On CentOS/RHEL/Fedora

# Then try configure again
./configure
make

Step 6: Customizing Your Build

The configure script often accepts options to customize how the software is built.

See available options:

1
./configure --help

This shows you all the options you can use to customize the build.

Common useful options:

1
2
3
4
5
6
7
8
# Install to a custom location instead of /usr/local
./configure --prefix=/opt/myprogram

# Enable or disable specific features
./configure --enable-ssl --disable-ipv6

# Specify custom library locations
./configure --with-openssl=/usr/local/ssl

Example with custom prefix:

1
2
3
./configure --prefix=$HOME/local
make
make install

This installs the software to $HOME/local instead of /usr/local, so you don’t need sudo privileges.

Step 7: Troubleshooting Common Problems

Problem: Configure fails with “C compiler cannot create executables”

Solution: Install the basic development tools:

1
2
sudo apt install build-essential  # Ubuntu/Debian
sudo dnf groupinstall "Development Tools"  # CentOS/RHEL/Fedora

Problem: Make fails with missing header files

Solution: Install development packages for the missing libraries:

1
2
3
# Find the right package
apt search packagename-dev
sudo apt install packagename-dev

Problem: “Permission denied” during make install

Solution: Use sudo, or configure with a prefix you can write to:

1
2
3
sudo make install
# or
./configure --prefix=$HOME/local

Problem: Program runs but can’t find shared libraries

Solution: Update your library path:

1
2
3
4
5
# Temporary fix
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH

# Permanent fix (add to ~/.bashrc)
echo 'export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH' >> ~/.bashrc

Step 8: Best Practices

Create a dedicated build user:

For serious development work, consider creating a separate user account for building software to avoid cluttering your main account:

1
2
sudo useradd -m builduser
sudo su - builduser

Keep source and builds organized:

1
2
3
mkdir -p ~/src ~/builds
# Download sources to ~/src
# Build in ~/builds

Always read the documentation:

1
2
3
4
# Check these files before building
cat README
cat INSTALL
cat CHANGELOG

Clean up after building:

1
2
3
4
5
# Remove build artifacts but keep the source
make clean

# Remove everything generated by configure
make distclean

Save your configure options:

When you find a configuration that works, save it:

1
2
3
# Save your configure command
echo './configure --prefix=/opt/myapp --enable-ssl' > my-config.sh
chmod +x my-config.sh

Understanding What Happens Behind the Scenes

When you run make, here’s what typically happens:

  1. Dependency checking: Make reads the Makefile and figures out which files have changed and need to be recompiled
  2. Compilation: Source files (.c) are compiled into object files (.o) using gcc
  3. Linking: Object files are linked together with required libraries to create the final executable
  4. Installation: Files are copied to their final destinations in the system

The beauty of make is that it only recompiles what’s necessary. If you change one source file in a large project, it only recompiles that file and re-links the final program, saving enormous amounts of time.

Wrapping Up

Building software from source might seem intimidating at first, but it follows predictable patterns. The vast majority of open-source software uses the same basic process:

  1. Download and extract the source code
  2. Read the documentation (README, INSTALL)
  3. Install any required dependencies
  4. Configure the build (either with ./configure or by editing the Makefile)
  5. Run make to compile
  6. Run make install to install

Once you’ve done this a few times, it becomes second nature. You’ll start to recognize common patterns and be able to troubleshoot issues more effectively.

The skills you’ve learned here – understanding compilers, build systems, and dependency management – are fundamental to working with Linux systems and will serve you well whether you’re a system administrator, developer, or just someone who wants to understand how their computer really works.

Remember, every piece of software on your Linux system was built this way at some point. You’re not just learning a technical skill; you’re learning how the entire open-source ecosystem works. And that’s pretty powerful knowledge to have.

Now go forth and build something awesome from source!