Makefile 배우기

4 hours ago 1

  • Makefile은 C/C++ 빌드 자동화 및 의존성 관리를 간소화하는 도구임
  • 타임스탬프를 활용한 변경 파일 검출 방식으로, 필요한 경우에만 컴파일 작업을 실행함
  • 규칙(rule), 명령어(command), 의존성(prerequisite) 등 핵심 구조를 예제와 함께 설명함
  • 자동 변수, 패턴 규칙, 변수 확장 같은 고급 기능도 실용적으로 다룸
  • 중간 규모 프로젝트용 실전 Makefile 템플릿을 통한 확장성과 관리의 중요성 소개함

Makefile 튜토리얼 가이드 소개

Makefile은 프로젝트 빌드 자동화와 의존성 관리를 담당하는 핵심 도구임. 다양한 숨은 규칙과 기호로 인해 처음 접할 때 복잡하게 느낄 수 있으나, 이 가이드는 주요 내용을 간결하고 직접 실행 가능한 예제로 정리함. 각 섹션별로 실습 기반 예시를 통한 이해가 가능함.


시작하기

Makefile의 존재 목적

  • Makefile은 대형 프로그램에서 변경된 부분만 재컴파일하는 데 활용됨
  • C/C++ 이외에도 여러 언어별 전용 빌드 도구가 존재하지만, Make는 일반적인 빌드 시나리오 전반에 활용됨
  • 변경된 파일을 감지해 필요한 작업만 실행하는 로직이 핵심임

Make의 대안 빌드 시스템

  • C/C++ 계열: SCons, CMake, Bazel, Ninja 등 여러 선택지가 있음
  • Java 계열: Ant, Maven, Gradle 등
  • Go, Rust, TypeScript 등도 자체 빌드 도구 제공
  • 파이썬, Ruby, JavaScript 등 인터프리터 언어는 컴파일이 필요 없어 Makefile과 같은 별도 관리 필요성 낮음

Make의 버전과 종류

  • 다양한 Make 구현체가 있으나, 본 가이드는 GNU Make(주로 Linux, MacOS에서 사용)에 최적화되어 있음
  • 예제는 GNU Make 3, 4 버전에 모두 호환

예제 실행 방법

  • 터미널에서 make 설치 후, 각 예시를 Makefile 파일로 저장 및 make 명령어 실행
  • Makefile 내의 명령어 줄은 반드시 탭 문자로 들여쓰기 필요

Makefile 기본 구문

규칙(Rule)의 구조

  • 타겟: 의존성(들)

    • 명령어
    • 명령어
  • 타겟: 빌드 결과 파일명(보통 하나)

  • 명령어: 실제 동작하는 쉘 스크립트(탭으로 시작)

  • 의존성: 타겟이 빌드되기 전에 반드시 준비되어야 할 파일 목록


Make의 본질

Hello World 예제

hello: echo "Hello, World" echo "This line will print if the file hello does not exist."
  • 타겟 hello는 의존성이 없고, 커맨드 2개를 실행함
  • make hello 실행 시, 파일 hello가 존재하지 않으면 명령어가 실행됨. 이미 파일이 있다면 실행하지 않음
  • 일반적으로 타겟=파일명이 일치하도록 작성됨

C 파일 컴파일 기본 예제

  1. blah.c 파일 생성(int main() { return 0; } 내용)
  2. 다음 Makefile 작성
blah: cc blah.c -o blah
  • make 실행 시, blah 타겟이 없다면 컴파일이 실행되어 blah 파일 생성됨
  • blah.c 변경 후에도 자동 재컴파일 X → 의존성 추가 필요

의존성 추가 방식

blah: blah.c cc blah.c -o blah
  • 이제 blah.c가 새로 변경됐다면, blah 타겟이 다시 빌드됨
  • 파일 타임스탬프를 변경 검출의 기준으로 사용함
  • 타임스탬프를 임의로 조작하면 의도와 다르게 작동할 수 있음

예제 추가

연결된 타겟 및 의존성 예제

blah: blah.o cc blah.o -o blah blah.o: blah.c cc -c blah.c -o blah.o blah.c: echo "int main() { return 0; }" > blah.c
  • 트리 구조로 의존성을 따라가며 각 단계별 생성 과정이 자동화됨

반드시 실행되는 타겟 예제

some_file: other_file echo "This will always run, and runs second" touch some_file other_file: echo "This will always run, and runs first"
  • other_file이 실제 파일로 생성되지 않으므로, some_file 명령이 매번 실행됨

Make clean

  • clean 타겟은 빌드 산출물을 삭제하는 용도로 자주 사용됨
  • Make에서 특별한 예약어는 아니며, 직접 명령어로 정의 필요
  • 만약 파일명이 clean이면 혼동될 수 있으므로, .PHONY 사용을 권장

예시:

some_file: touch some_file clean: rm -f some_file

변수 처리

  • 변수는 항상 문자열.
  • 보통 :=을 권장하며, =, ?=, += 등의 다양한 대입 방식 존재
  • 사용 예시:
files := file1 file2 some_file: $(files) echo "Look at this variable: " $(files) touch some_file file1: touch file1 file2: touch file2 clean: rm -f file1 file2 some_file
  • 변수 참조 방식: $(variable) 또는 ${variable}
  • Makefile 내 따옴표는 Make 자체에서는 의미 없음(단, 쉘 명령어에서는 필요)

타겟 관리

all 타겟

  • 여러 타겟을 한꺼번에 실행하려면, 첫 번째(디폴트) 타겟에 속성 부여
all: one two three one: touch one two: touch two three: touch three clean: rm -f one two three

다중 타겟 및 자동 변수

  • 다수 타겟에 대해 각자 개별 명령 실행 가능. $@는 현재 타겟명을 가짐
all: f1.o f2.o f1.o f2.o: echo $@

자동 변수와 와일드카드

* 와일드카드

  • *는 파일 시스템상 이름을 직접 탐색
  • 반드시 wildcard 함수로 감싸서 사용 권장
print: $(wildcard *.c) ls -la $?
  • 변수 정의에서 직접 * 사용 금지
thing_wrong := *.o thing_right := $(wildcard *.o)

% 와일드카드

  • 주로 패턴 규칙에서 사용, 지정 패턴을 추출하여 확장 가능

Fancy Rules

암시적(Implicit) 규칙

  • Make는 C/C++ 빌드와 관련된 여러 숨은 기본 규칙을 내장함
  • 대표 변수: CC, CXX, CFLAGS, CPPFLAGS, LDFLAGS 등
  • C 예제:
CC = gcc CFLAGS = -g blah: blah.o blah.c: echo "int main() { return 0; }" > blah.c clean: rm -f blah*

Static Pattern Rules

  • 동일한 패턴을 따르는 다수 규칙을 간결하게 작성 가능
objects = foo.o bar.o all.o all: $(objects) $(CC) $^ -o all $(objects): %.o: %.c $(CC) -c $^ -o $@ all.c: echo "int main() { return 0; }" > all.c %.c: touch $@ clean: rm -f *.c *.o all

Static Pattern Rules + filter 함수

  • filter를 활용하면 특정 확장자 패턴에 맞는 대상만 선택 가능
obj_files = foo.result bar.o lose.o src_files = foo.raw bar.c lose.c all: $(obj_files) .PHONY: all $(filter %.o,$(obj_files)): %.o: %.c echo "target: $@ prereq: $<" $(filter %.result,$(obj_files)): %.result: %.raw echo "target: $@ prereq: $<" %.c %.raw: touch $@ clean: rm -f $(src_files)

패턴 규칙

  • %를 사용해 간편하게 규칙을 작성 가능
%.o : %.c $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@ %.c: touch $@

Double-Colon Rules

  • 동일 타겟에 여러 규칙을 동시에 정의하고 각각 별도로 실행 가능
all: blah blah:: echo "hello" blah:: echo "hello again"

명령어와 실행

명령어 출력 제어

  • 명령 앞에 @ 추가 시, 출력 생략
  • make에 -s 옵션 사용시 전체 명령 생략

각 명령어 실행 환경

  • 각 makefile 내 명령어 줄은 별도의 쉘에서 독립적으로 실행

쉘 종류 지정

  • 기본은 /bin/sh이며, 필요시 SHELL=/bin/bash 식으로 변경

달러 기호 이스케이프

  • Makefile 내부에서 쉘 변수를 쓰려면 $$로 이스케이프 필요

에러 처리

  • -k: 에러 발생 시 다음 작업 계속 진행
  • 커맨드 앞에 - 추가 시 실패해도 무시
  • -i 옵션: 전체 명령이 실패해도 무시

make 중단 시 행동

  • Ctrl+C 등으로 강제 중단 시, Make가 만든 신규 타겟을 삭제

재귀 make

  • $(MAKE)를 사용하면 환경 변수와 플래그가 유지된 상태로 make 호출 가능

변수와 환경

환경 변수 자동 읽기

  • Make는 실행 시점의 환경 변수도 자동으로 import해서 사용함

export, .EXPORT_ALL_VARIABLES

  • export 키워드를 활용하여 shell 및 하위 make 명령에도 변수 전달 가능
  • .EXPORT_ALL_VARIABLES:를 추가하면 모든 변수를 한꺼번에 export

Variables Pt. 2

변수의 종류와 확장

  • Recursive(=) : 변수 사용 시점에 참조
  • Simply expanded(:=) : 변수 선언 시점에 값 확정

변수 초기화/추가/미정의

  • ?=: 변수 미설정 시 최초 1회만 지정
  • +=: 기존 값에 내용 추가

undefined 변수는 빈 문자열로 처리됨


명령행 인자, override

  • override를 쓰면 명령행 인자로 들어온 값을 강제로 덮을 수 있음

define/endef로 여러 줄 명령 정의

  • 여러 줄로 된 명령 집합을 변수처럼 저장/실행 가능

타겟별 변수, 패턴별 변수

  • 특정 타겟이나 패턴에 한정하여 변수 값을 다르게 설정 가능

조건문

if/else/ifeq/ifdef/ifndef

  • 조건에 따른 분기 처리 지원
  • strip, findstring 등과 결합하여 조건 작성

함수

내장 함수

  • 텍스트 처리 중심: subst, patsubst, foreach, if, call, shell, filter 등
  • 함수 호출 방식: $(함수이름, 인자)

예시:

bar := ${subst not,"totally", "I am not superman"}
  • 첫 번째 인자(찾을 문자열), 두 번째(치환할 문자열), 세 번째(대상 텍스트)

filter, filter-out, nested filter

  • 특정 리스트에서 패턴 매칭 요소만 남기거나 필터링에 활용

기타 고급 기능

include

  • 외부 Makefile을 include 디렉티브로 포함 가능

vpath

  • 특정 패턴 파일이 존재하는 디렉터리 지정

멀티라인 명령

  • 백슬래시(\)로 여러 줄에 걸쳐 명령 작성 가능

.PHONY

  • 명확하게 파일명이 아닌 대상 타겟임을 선언해 혼동 방지

.DELETE_ON_ERROR

  • Make에서 규칙 실패 시 해당 타겟 자동 삭제

Makefile Cookbook: 실전 예제

  • 중간 규모 C/C++ 프로젝트에서 자동 의존성, 다중 소스 디렉터리, 효율적인 객체/빌드 디렉터리 관리에 최적화된 Makefile 템플릿임
  • C/C++ 소스 파일을 일괄적으로 src/ 폴더에 두면, 자동으로 객체 파일, 의존성 파일 및 최종 실행 파일을 관리함
  • 예시 Makefile에는 -MMD -MP 옵션 기반의 의존성 자동 포함 기능, header/소스 폴더 자동 인식, 빌드 실패 시 파일 삭제, phony 타겟 선언 등 다양한 실전적 모범 패턴이 적용됨
TARGET_EXEC := final_program BUILD_DIR := ./build SRC_DIRS := ./src SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s') OBJS := $(SRCS:%=$(BUILD_DIR)/%.o) DEPS := $(OBJS:.o=.d) INC_DIRS := $(shell find $(SRC_DIRS) -type d) INC_FLAGS := $(addprefix -I,$(INC_DIRS)) CPPFLAGS := $(INC_FLAGS) -MMD -MP $(BUILD_DIR)/$(TARGET_EXEC): $(OBJS) $(CXX) $(OBJS) -o $@ $(LDFLAGS) $(BUILD_DIR)/%.c.o: %.c mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ $(BUILD_DIR)/%.cpp.o: %.cpp mkdir -p $(dir $@) $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ .PHONY: clean clean: rm -r $(BUILD_DIR) -include $(DEPS)

이 가이드는 Makefile에 대해 실제로 동작하며 이해할 수 있는 풍부한 예제와 함께 전체 동작 원리와 실무적인 작성법을 포괄적으로 설명함.

Read Entire Article