Charting the Path to Efficiency: The Significance of Makefiles in Software Development

Charting the Path to Efficiency: The Significance of Makefiles in Software Development

·

4 min read

❓The Significance of Makefiles

Have you ever wondered how to streamline your software development workflow and enhance your team's efficiency? One of the often-overlooked tools that can make a significant difference is the Makefile. In this article, we will explore the importance of Makefiles in software development and how they can help you save time and reduce errors in your projects.

📁 What is a Makefile?

A Makefile is a simple yet powerful script that automates the process of building, compiling, and managing software projects. It consists of rules and dependencies, allowing developers to define how source code files should be transformed into executable programs or other target files. Makefiles are widely used in Unix-like operating systems and are also supported on Windows through tools like GNU Make.

😇 Simplifying Compilation and Build Processes

One of the primary purposes of Makefiles is to simplify complex compilation and build processes. They help in managing dependencies, ensuring that only the necessary source files are recompiled when changes are made. This not only saves time but also reduces the risk of introducing errors into the codebase.

# Example Makefile for a C++ project
CXX = g++
CXXFLAGS = -std=c++11 -Wall

my_program: main.cpp utils.cpp
    $(CXX) $(CXXFLAGS) -o my_program main.cpp utils.cpp

🧰 Examples

Here is a few examples of Makefiles i've built for myself

## ansible

run:
    sh scripts/check_and_run.sh || true

playbook:
    ansible-playbook ansible/playbooks/$(module)/$(playbook).yml -i ansible/inventory/$(inventory).ini

playbook-suite:
    ansible-playbook ansible/suite/$(playbook).yml -i ansible/inventory/$(inventory).ini

# https://www.digitalocean.com/community/tutorials/how-to-access-system-information-facts-in-ansible-playbooks

facts:
    ansible all -i ansible/inventory/$(inventory).ini -m setup

## aws

include config/aws.mk

describe:
    aws ec2 describe-instances | jq '.Reservations[].Instances[] | {InstanceId, State, PublicIpAddress, PrivateIpAddress, Tags}'

# https://docs.aws.amazon.com/cli/latest/reference/ec2/run-instances.html

create:
    aws ec2 run-instances \
    --image-id $(image_id) \
    --instance-type $(instance_type) \
    --key-name $(key_name) \
    --security-group-ids $(security_group_ids) \
    --subnet-id $(subnet_id) \
    --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=$(name)}]' \
    --associate-public-ip-address

terminate:
    aws ec2 terminate-instances --instance-id $(instance_id)

You can include variables with the include keyword and the file .mk as you may noticed that i'm using there.

It looks like this:

image_id ?= ami-0261755bbcb8c4a84
instance_type ?= t2.micro
key_name ?= jonathan@jonathan
security_group_ids ?= sg-0b156e283023b9fc2
subnet_id ?= subnet-0bcc53200daecb50e
vpc_id ?= vpc-074271020d1e20c8f
name ?= testing

If you don't expecify a value you can have a default one so I only type make create and have a fast and reliable ec2 instance to test my playbooks

Then I can delete it with make terminate instance_id="<id>"

You can use it anywhere for example here is one really easy that I normally use in python apps built with pip

main: install
    $(MAKE) start

install:
    pip install -r requirements.txt

requirements:
    pip freeze > requirements.txt

dev:
    uvicorn main:app --reload --port 4500

start:
    uvicorn main:app --host 0.0.0.0 --port 4500

build:
    docker compose up -d --build

up:
    docker compose up -d

👿 Complex Example

include config.mk

## Application

.PHONY: deploy
deploy: 
    $(MAKE) start action=apply environment=$(environment)

.PHONY: destroy
destroy:
    $(MAKE) start action=destroy environment=$(environment)

.PHONY: list
list:
    $(MAKE) start action=show environment=$(environment)

install:
    @echo "Running installation script..."
    chmod +x scripts/run.sh
    scripts/run.sh

### S3

community:
    @if ! ansible-galaxy collection list | grep -i 'community.aws'; then \
        ansible-galaxy collection install community.aws; \
    fi
    @if ! pip show boto3; then \
        pip install --user boto3; \
    fi
    @echo "Ansible Galaxy community.aws and boto3 already installed"

git_submodule:
    git submodule add $(submodule) app/$(app_name)

build_app:
    cd app/$(app_name) && npm i && npm run build

# Requires boto3 installed with pip
# https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#installation

echo:
    @echo "🪣 Bucket name: $(bucket_name)"
    @echo "🍎 App name: $(app_name)"
    @echo "📩 Environment: $(environment)"

ansible_list:
    $(MAKE) echo
    ansible-playbook ansible/playbook/s3_list.yml -e "bucket_name=$(bucket_name)" -e "access_key=$(access_key)" -e "secret_key=$(secret_key)" -i ansible/inventory/hosts

.PHONY: ansible_sync
ansible_sync:
    $(MAKE) echo
    ansible-playbook ansible/playbook/s3_sync.yml -e "bucket_name=$(bucket_name)" -e "app_name=$(app_name)" -i ansible/inventory/hosts

.PHONY: ansible_rm
ansible_rm:
    $(MAKE) echo
    ansible-playbook ansible/playbook/s3_remove.yml -e "bucket_name=$(bucket_name)" -i ansible/inventory/hosts

### Terraform

upgrade:
    cd terraform && terraform init -upgrade

#### init - get - output ####
start_noargs:
    @echo "Running terraform $(action)... with no args"
    cd terraform && terraform $(action)

#### apply - destroy - plan - show - refresh ####
.PHONY: start    
start:

ifeq ($(action), show)
    cd terraform && terraform show -json -compact-warnings | jq -r '(.$(jsonnames) | tostring) + " | " + (.$(jsonvalues) | tostring)'
else
    @echo "Running terraform $(action)..."
    cd terraform && terraform $(action) -var-file="config/$(environment).tfvars" -compact-warnings
endif

This one contains conditional executions, .PHONY targets, scripting, and many other things to make it clear. It may not be immediately obvious, but if you start small and build it piece by piece, you will notice that it is not that difficult. Additionally, when you encounter a project and want to build it from source, you will likely need to examine the makefile and understand what is happening.

🏁 Conclusion

Makefiles are a valuable asset in software development. They simplify complex processes, automate tasks, and reduce errors by managing dependencies. The examples provided illustrate how Makefiles can enhance productivity, from AWS resource management to application deployment. Whether you're working on a small project or a large-scale development, Makefiles are a powerful tool to streamline your workflow, save time, and maintain code quality. Embrace Makefiles to automate and simplify your development tasks for improved project management.