GNU Make

GNU Make는 소프트웨어 빌드 자동화를 위한 표준 도구입니다. Makefile 문법, 변수 할당 방식, 함수, 패턴/암묵적 규칙, 타겟 지정 변수, 순서 전용 의존성, 그룹 타겟, Makefile 지시자, 성능 최적화 등 GNU Make의 모든 기능을 GNU Make Manual 4.4를 기반으로 상세히 설명합니다.

관련 표준: POSIX.1-2017 (make 명령), GNU Make Manual 4.4 — GNU Make는 POSIX make를 확장한 가장 널리 사용되는 빌드 도구입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 빌드 시스템커널 개발 도구 문서를 먼저 읽으세요. 빌드 문제는 의존성 그래프와 증분 빌드 규칙이 원인이 되는 경우가 많으므로, 타깃/변수/규칙 평가 순서를 먼저 이해해야 합니다.
일상 비유: 이 주제는 조립 라인 작업 지시서와 비슷합니다. 순서표가 틀리면 앞 공정 결과가 뒤 공정에 반영되지 않듯이, Make 규칙도 의존성 선언 정확도가 결과를 결정합니다.

핵심 요약

  • 타겟(Target) — 만들고자 하는 파일 또는 작업 이름 (예: program, all, clean)
  • 의존성(Prerequisites) — 타겟을 만들기 전에 필요한 파일들 (예: main.c, utils.h)
  • 레시피(Recipe) — 타겟을 생성하는 셸 명령어 (반드시 탭 문자로 시작, 공백 아님!)
  • 변수 — 반복되는 값을 저장 (CC = gcc, CFLAGS = -Wall). 4가지 할당 방식: =, :=, ?=, +=
  • 패턴 규칙%.o: %.c 형태로 여러 파일에 동일한 빌드 규칙 적용
  • 자동 변수$@(타겟), $<(첫 의존성), $^(모든 의존성)로 반복 코드 제거

단계별 이해

  1. 1단계: 단일 파일 컴파일
    가장 간단한 Makefile부터 시작합니다.
    hello: hello.c
    	gcc -o hello hello.c
    
    make hello 실행 시 hello.c가 변경되었을 때만 다시 컴파일됩니다. 파일을 수정하지 않고 다시 make hello를 실행하면 "Nothing to be done"이 출력됩니다.
  2. 2단계: 변수 도입으로 반복 제거
    컴파일러와 옵션을 변수로 분리하면 수정이 쉬워집니다.
    CC = gcc
    CFLAGS = -Wall -g
    
    hello: hello.c
    	$(CC) $(CFLAGS) -o hello hello.c
    
    이제 컴파일러를 clang으로 바꾸려면 CC = clang 한 줄만 수정하면 됩니다.
  3. 3단계: 다중 파일 프로젝트
    여러 소스 파일을 오브젝트 파일로 먼저 컴파일한 후 링크합니다.
    CC = gcc
    CFLAGS = -Wall -O2
    
    program: main.o utils.o
    	$(CC) -o $@ $^
    
    main.o: main.c header.h
    	$(CC) $(CFLAGS) -c main.c
    
    utils.o: utils.c header.h
    	$(CC) $(CFLAGS) -c utils.c
    
    clean:
    	rm -f *.o program
    
    .PHONY: clean
    
    header.h를 수정하면 Make가 자동으로 main.outils.o를 다시 빌드합니다.
  4. 4단계: 패턴 규칙으로 간소화
    모든 .c 파일에 동일한 규칙을 적용하여 코드 중복을 제거합니다.
    SRCS = main.c utils.c parser.c
    OBJS = $(SRCS:.c=.o)
    
    program: $(OBJS)
    	$(CC) -o $@ $^
    
    # 패턴 규칙: 모든 .o는 .c에서 생성
    %.o: %.c
    	$(CC) $(CFLAGS) -c $<
    
    이제 새 소스 파일을 추가해도 SRCS 변수만 수정하면 됩니다.
  5. 5단계: 자동 의존성 관리
    헤더 파일 의존성을 수동으로 관리하는 것은 실수하기 쉽습니다. GCC의 -MMD 옵션으로 자동화합니다.
    DEPS = $(SRCS:.c=.d)
    
    %.o: %.c
    	$(CC) $(CFLAGS) -MMD -MP -c $<
    
    -include $(DEPS)
    
    이제 헤더 파일 의존성이 .d 파일에 자동 생성되어, 헤더 수정 시 관련 소스가 자동으로 재컴파일됩니다.

개요 (Overview)

GNU Make는 1988년 Richard Stallman과 Roland McGrath가 개발한 빌드 자동화 도구로, Linux 커널을 포함한 대부분의 오픈소스 프로젝트에서 사용됩니다. Make는 파일 간의 의존성 관계를 선언하고, 변경된 파일만 다시 빌드하여 효율적인 컴파일 과정을 제공합니다.

왜 Make를 사용하는가?

ℹ️

Linux 커널은 약 70,000개의 소스 파일을 관리하는 Kbuild 시스템을 GNU Make 위에 구축했습니다. Kbuild의 상세 내용은 빌드 시스템 페이지를 참고하세요.

Make 증분 빌드 타임스탬프 비교 플로차트 make 타겟 실행 요청 타겟 파일이 존재하는가? NO YES 의존성 타임스탬프가 타겟보다 새로운가? (mtime 비교) YES NO 레시피 실행 타겟 파일 없음 레시피 실행 변경된 파일 감지 빌드 생략 "Nothing to be done" 타겟 없음 → 무조건 빌드 변경 감지 → 선택적 재빌드
그림: Make 증분 빌드 동작 — 파일 타임스탬프 비교로 불필요한 재컴파일을 방지

기본 개념

Makefile은 다음 세 가지 핵심 요소로 구성됩니다.

  1. 타겟(Target) — 빌드 결과물이나 작업 이름 (예: main.o, all, clean)
  2. 의존성(Prerequisites) — 타겟을 빌드하기 전에 필요한 파일들 (예: main.c, header.h)
  3. 레시피(Recipe) — 타겟을 생성하기 위한 셸 명령어 (반드시 탭 문자로 시작)
# 기본 Makefile 구조
target: prerequisite1 prerequisite2
	recipe-command1
	recipe-command2
Makefile 기본 구조 해부도 program : main.o utils.o TAB $(CC) -o $@ $^ 타겟 (Target) 만들어낼 파일/작업 의존성 (Prerequisites) 빌드 전 필요한 파일들 탭 문자 스페이스 불가! 가장 흔한 실수 레시피 (Recipe) 타겟을 만드는 셸 명령 여러 줄 가능 Make 실행 원리 ① 타겟 파일이 없다 → 레시피 실행 ② 의존성이 타겟보다 새롭다 → 재빌드 ③ 모두 최신 → "Nothing to be done" make program → make가 자동 판단 변경된 파일만 선택적으로 재빌드!
그림: Makefile 기본 구조 — 타겟, 의존성, 레시피의 역할과 탭 문자 필수 위치

기본 문법 (Basic Syntax)

간단한 Makefile 예제

# 변수 정의
CC = gcc
CFLAGS = -Wall -O2

# 기본 타겟
all: program

# 실행 파일 생성
program: main.o utils.o
	$(CC) -o $@ $^

# 오브젝트 파일 생성
main.o: main.c header.h
	$(CC) $(CFLAGS) -c main.c

utils.o: utils.c header.h
	$(CC) $(CFLAGS) -c utils.c

# PHONY 타겟
.PHONY: clean
clean:
	rm -f *.o program
Make 의존성 그래프 (DAG) 의존성 그래프: make program 실행 시 빌드 순서 program 최종 실행 파일 main.o 오브젝트 파일 utils.o 오브젝트 파일 main.c 소스 파일 header.h 공유 헤더 파일 utils.c 소스 파일 화살표 A→B: A가 B에 의존 최종 타겟 중간 타겟(.o) 공유 헤더 header.h 수정 → main.o, utils.o 모두 재빌드 1 1 1 2 2 3
그림: Make 의존성 그래프 — 숫자는 빌드 순서. header.h 수정 시 두 .o 파일이 모두 재빌드됨

자동 변수 (Automatic Variables)

Make는 레시피 실행 시 자동으로 설정되는 특수 변수를 제공합니다.

변수 의미 예제
$@ 타겟 이름 program: main.o에서 $@program
$< 첫 번째 의존성 main.o: main.c header.h에서 $<main.c
$^ 모든 의존성 (중복 제거) program: main.o utils.o에서 $^main.o utils.o
$+ 모든 의존성 (중복 포함) 링커 명령어에서 순서가 중요할 때 사용
$? 타겟보다 새로운 의존성 증분 빌드 최적화에 사용
$* 패턴 규칙의 스템(stem) %.o: %.c에서 main.o일 때 $*main
Make 자동 변수 위치 시각화 규칙 헤더: program : main.o utils.o config.h 레시피: $(CC) -o $@ $< $^ $@ 타겟 이름 = "program" $< 첫 번째 의존성 = "main.o" $^ 모든 의존성 (중복 제거) = "main.o utils.o config.h" $? 타겟보다 새로운 의존성 예: config.h 변경 시 "config.h" $* 패턴 규칙의 스템(stem) — %.o: %.c 에서 main.o 빌드 시 $* = "main" ※ $(@D), $(@F): 타겟의 디렉토리/파일명 분리. $(<D), $(<F): 첫 의존성의 디렉토리/파일명 분리
그림: Make 자동 변수 시각화 — 규칙 구성 요소와 각 자동 변수가 가리키는 대상
💡

디렉토리/파일명 추출: $(@D)는 타겟의 디렉토리, $(@F)는 파일명만 추출합니다. 예: src/main.o에서 $(@D)src, $(@F)main.o입니다.

변수 (Variables)

변수 할당 방식

GNU Make는 4가지 변수 할당 연산자를 제공합니다.

연산자 이름 동작 예제
= 재귀 확장 (Recursive) 사용 시점에 평가 (lazy evaluation) CFLAGS = $(OPT) -Wall
:= 즉시 확장 (Simple) 정의 시점에 즉시 평가 SRCS := $(wildcard *.c)
?= 조건부 할당 변수가 정의되지 않았을 때만 할당 CC ?= gcc
+= 추가 (Append) 기존 값에 추가 CFLAGS += -g
:::= 즉시 확장 후 이스케이프 즉시 확장하되 결과의 $$$로 이스케이프. BSD make 호환 (POSIX Issue 8) OUT :::= $(var)
!= 쉘 할당 (Shell Assignment) 우변을 즉시 쉘에 전달하고 결과를 재귀 확장 변수로 저장 UNAME != uname -s
# 재귀 확장 vs 즉시 확장
OPT = -O2
CFLAGS_LAZY = $(OPT) -Wall    # 사용 시점에 OPT 평가
CFLAGS_EAGER := $(OPT) -Wall  # 지금 바로 "-O2 -Wall"로 확장

OPT = -O3  # 나중에 변경

# 결과:
# CFLAGS_LAZY는 "-O3 -Wall"
# CFLAGS_EAGER는 "-O2 -Wall" (변경 영향 없음)

변수 할당 의사결정 플로차트

상황에 따라 적절한 변수 할당 연산자를 선택하는 것이 중요합니다. 아래 플로차트는 최적의 선택을 안내합니다.

Make 변수 할당 연산자 선택 플로차트 변수 할당? 변수가 이미 정의됨? YES 기존 값에 추가? YES += 추가 할당 CFLAGS += -g NO 재할당 =, :=, ::= 기존 값 덮어쓰기 NO 즉시 평가 필요? YES := or ::= 즉시 확장 SRC := $(wildcard *.c) 함수 호출 1회만 NO 사용자 오버라이드 허용? YES ?= 조건부 할당 CC ?= gcc NO = 재귀 확장 CFLAGS = $(OPT) -Wall 유연한 참조
그림: Make 변수 할당 연산자 선택 플로차트
사용 사례 권장 연산자 이유
shell/wildcard 함수 호출 := 함수를 한 번만 실행 (성능)
컴파일러/플래그 기본값 ?= 사용자 환경 변수 존중
다른 변수 참조 = 참조 변수 변경 시 자동 반영
플래그 누적 += 기존 값 보존하며 추가

변수 확장 동작 요약

Make가 Makefile을 읽을 때 두 단계로 처리합니다: 읽기 단계(Read Phase)실행 단계(Target-Update Phase). 각 할당 방식은 이 두 단계에서 다르게 동작합니다.

연산자 좌변 평가 우변 평가 변수 종류
= 즉시 사용 시점 (지연) 재귀 확장
:= / ::= 즉시 즉시 단순 확장
:::= 즉시 즉시 (후 이스케이프) 재귀 확장 (BSD 호환)
?= 즉시 사용 시점 (지연) 재귀 확장 (미정의 시만)
+= 즉시 이전 변수 종류에 따름 이전 종류 유지
!= 즉시 즉시 (쉘 실행) 재귀 확장
# :::= 예제: 즉시 확장 후 $ 이스케이프
var = first
OUT :::= $(var)     # 즉시 "first"로 확장됨
var = second          # OUT에 영향 없음
# $(OUT) → "first"

# != 예제: 쉘 명령어 결과 저장
UNAME != uname -s    # 쉘에서 실행: Linux, Darwin 등
DATE  != date +%Y-%m-%d
# 주의: != 로 저장된 변수는 재귀 확장 변수이므로
# 참조할 때마다 재평가됨 (쉘은 재실행 안 됨, 이미 저장된 값)

계산된 변수명 (Computed Variable Names)

변수명 자체를 다른 변수로 계산할 수 있습니다. 이를 중첩 변수 참조(Nested Variable Reference)라고 합니다.

# 기본 예: 변수명을 다른 변수로 지정
x = y
y = hello
result := $($(x))   # $(x) → y → $(y) → hello

# 실용적 활용: 아키텍처별 플래그 선택
CFLAGS_x86   = -march=x86-64
CFLAGS_arm   = -march=armv8-a
CFLAGS_riscv = -march=rv64gc

ARCH   ?= x86
CFLAGS += $(CFLAGS_$(ARCH))  # ARCH=arm이면 CFLAGS_arm 사용

# 다층 중첩 (3단계)
x = y
y = z
z = final_value
a := $($($(x)))              # → final_value

# 설정 기반 동적 변수 선택
a_dirs := dira dirb
1_dirs := dir1 dir2
a1 := a
df := dirs
result := $($(a1)_$(df))    # → "dira dirb"

치환 참조 (Substitution Reference)

변수 값의 일부를 패턴으로 치환할 수 있습니다.

# 확장자 치환
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)  # main.o utils.o parser.o

# 패턴 치환
SRCS = src/main.c src/utils.c
OBJS = $(SRCS:src/%.c=build/%.o)  # build/main.o build/utils.o

# patsubst 함수 사용
OBJS = $(patsubst %.c,%.o,$(SRCS))

환경 변수

Make는 쉘 환경 변수를 자동으로 가져옵니다. Makefile 내 정의가 우선순위를 가지며, -e 옵션으로 환경 변수를 우선할 수 있습니다.

# 환경 변수 사용
CC ?= $(CROSS_COMPILE)gcc  # CROSS_COMPILE이 환경에서 설정되면 사용

타겟 지정 변수 (Target-specific Variables)

타겟 지정 변수는 특정 타겟과 그 의존성의 레시피 내에서만 유효한 변수를 정의합니다. 전역 변수와 동일한 이름을 사용하더라도 해당 타겟에서만 다른 값을 가집니다.

# 문법: TARGET : VARIABLE-ASSIGNMENT

# debug 타겟만 -g 플래그 사용
debug: CFLAGS = -g -O0 -DDEBUG
debug: program

release: CFLAGS = -O2 -DNDEBUG
release: program

# 의존성에도 전파됨!
prog: CFLAGS = -g
prog: prog.o foo.o bar.o
# → prog.o, foo.o, bar.o도 CFLAGS=-g로 컴파일됨

# 여러 키워드 조합 가능
install: override CFLAGS += -DINSTALL
test:    export VERBOSE = 1    # 자식 프로세스에도 export

# 상속 억제: private 키워드
EXTRA_CFLAGS =
prog: private EXTRA_CFLAGS = -L/usr/local/lib
prog: a.o b.o
# → a.o, b.o는 EXTRA_CFLAGS를 상속받지 않음
💡

활용 패턴: 하나의 Makefile에서 make debugmake release를 분리할 때 매우 유용합니다. 타겟 지정 변수는 의존성에도 자동으로 전파되므로, 최상위 타겟에만 설정하면 전체 빌드에 적용됩니다.

패턴 지정 변수 (Pattern-specific Variables)

패턴 지정 변수는 패턴과 일치하는 모든 타겟에 적용됩니다. 타겟 지정 변수와 유사하지만 와일드카드 패턴을 사용합니다.

# 문법: PATTERN : VARIABLE-ASSIGNMENT

# 모든 .o 파일을 최적화로 컴파일
%.o: CFLAGS := -O2

# lib/에 있는 .o는 위치 독립 코드
lib/%.o: CFLAGS := -fPIC -g

# 우선순위: 타겟 지정 > 패턴 지정 (더 긴 스템이 우선)
lib/%.o: CFLAGS := -fPIC -g     # 스템: lib/ 포함, 더 구체적
%.o: CFLAGS := -g               # 일반 .o 파일

# 예: 테스트 바이너리만 커버리지 활성화
test_%: CFLAGS += --coverage
test_%: LDFLAGS += --coverage

패턴 규칙 (Pattern Rules)

패턴 규칙은 % 와일드카드를 사용하여 여러 타겟에 동일한 빌드 규칙을 적용합니다.

암묵적 규칙 (Implicit Rules)

GNU Make는 일반적인 컴파일 작업을 위한 기본 패턴 규칙을 내장하고 있습니다.

# 내장 암묵적 규칙 (사용자가 정의하지 않아도 동작)
# %.o: %.c
#     $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

# 암묵적 규칙 덕분에 이렇게만 써도 동작
CC = gcc
CFLAGS = -Wall -O2

program: main.o utils.o
	$(CC) -o $@ $^

# main.o와 utils.o는 암묵적 규칙으로 자동 빌드됨

사용자 정의 패턴 규칙

# 모든 .c 파일을 .o로 컴파일하는 규칙
%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

# 여러 확장자를 처리하는 패턴
%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c -o $@ $<

# 디렉토리 구조가 있는 경우
build/%.o: src/%.c
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) -c -o $@ $<

정적 패턴 규칙 (Static Pattern Rules)

특정 타겟 목록에만 적용되는 패턴 규칙입니다.

OBJS = main.o utils.o parser.o

# OBJS에 속한 파일들에만 이 규칙 적용
$(OBJS): %.o: %.c
	$(CC) $(CFLAGS) -MMD -c -o $@ $<

# 다른 디렉토리 매핑
BUILD_OBJS = $(addprefix build/,$(OBJS))
$(BUILD_OBJS): build/%.o: src/%.c
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) -c -o $@ $<

순서 전용 의존성 (Order-only Prerequisites)

파이프 기호 | 오른쪽에 지정된 의존성은 순서 전용입니다. 타겟의 빌드 순서를 보장하지만, 해당 의존성이 타겟보다 새로워도 타겟을 다시 빌드하지 않습니다.

# 문법: TARGETS : NORMAL-DEPS | ORDER-ONLY-DEPS

# 출력 디렉토리가 없으면 먼저 생성하되, 디렉토리 타임스탬프 변경으로
# .o 파일을 재빌드하지는 않음
OBJDIR := build
OBJS   := $(OBJDIR)/foo.o $(OBJDIR)/bar.o

$(OBJDIR)/%.o : %.c | $(OBJDIR)
	$(CC) $(CFLAGS) -c -o $@ $<

all: $(OBJS)

$(OBJS): | $(OBJDIR)

$(OBJDIR):
	mkdir -p $@

# 일반 의존성: 해당 파일이 변경되면 타겟도 재빌드
# 순서 의존성: 해당 파일이 없으면 먼저 만들지만, 변경되어도 재빌드 안 함
ℹ️

디렉토리 의존성 패턴: 빌드 출력 디렉토리는 파일 추가/삭제마다 타임스탬프가 바뀝니다. 일반 의존성으로 선언하면 모든 파일이 불필요하게 재빌드됩니다. 순서 전용 의존성(| $(OBJDIR))으로 선언하면 이 문제를 완벽하게 해결합니다.

Make 순서 전용 의존성 vs 일반 의존성 비교 일반 의존성 (Normal Prerequisites) 순서 전용 의존성 (Order-only) build/foo.o : foo.c build/ build/ 가 일반 의존성 → 타임스탬프 감시 build/foo.o : foo.c | build/ | 뒤: 순서 보장만, 타임스탬프 무시 시나리오: build/ 디렉토리에 새 파일 추가됨 → build/ mtime 갱신 ① build/ mtime 갱신 감지 ② build/ 가 build/foo.o 보다 새로움 ③ build/foo.o 재빌드 트리거! 불필요한 재컴파일 발생! foo.c 변경 없이도 재빌드 ① build/ mtime 갱신 감지 ② 순서 전용 → 타임스탬프 비교 생략 ③ foo.c 변경 없음 → 재빌드 없음 빌드 유지, 재컴파일 없음! 디렉토리 존재 보장 + 효율 유지 순서 전용(|): 타겟 빌드 전 의존성 존재 보장 + 의존성 변경이 타겟 재빌드를 트리거하지 않음
그림: 일반 의존성 vs 순서 전용 의존성 — 디렉토리 타임스탬프로 인한 불필요한 재빌드 방지

그룹 타겟 (Grouped Targets)

단일 레시피 호출로 여러 파일이 동시에 생성될 때 &: 구분자를 사용하여 그룹 타겟으로 선언합니다. Make는 하나의 타겟이 필요하면 전체 레시피를 한 번만 실행합니다.

# 문법: TARGET1 TARGET2 &: PREREQUISITES

# protobuf 컴파일러는 .pb.h와 .pb.cc를 동시에 생성
foo.pb.h foo.pb.cc &: foo.proto
	protoc --cpp_out=. foo.proto

# 여러 출력 파일 동시 생성
foo bar biz &: baz boz
	generate_all $^

# $@ 는 트리거된 특정 타겟을 가리킴 (주의 필요)
# 그룹 더블 콜론: 동일 타겟을 여러 그룹에 포함 가능
foo bar &:: source1
	generate_foo_bar source1

foo baz &:: source2
	generate_foo_baz source2

더블 콜론 규칙 (Double-colon Rules)

더블 콜론 ::을 사용하면 동일한 타겟에 대해 독립적인 규칙 여러 개를 정의할 수 있습니다. 각 규칙은 자신의 의존성이 타겟보다 새로울 때 독립적으로 실행됩니다.

# 동일 타겟에 대한 두 가지 독립 업데이트 방법
config.h:: version.h
	@echo "Updating config from version.h"
	generate_config_from_version $<

config.h:: platform.h
	@echo "Updating config from platform.h"
	generate_config_from_platform $<

# 의존성 없는 더블 콜론: 항상 실행
clean::
	rm -f *.o

clean::
	rm -f *.d

# 단일 콜론 vs 더블 콜론:
# :: 규칙은 서로 독립적, 각 규칙 조건이 충족되면 별도로 실행
# :  규칙은 마지막 규칙의 레시피만 사용 (경고 발생)
⚠️

동일 타겟에 대한 규칙은 모두 ::이거나 모두 :이어야 합니다. 혼용하면 에러가 발생합니다. :: 규칙은 비교적 드물게 사용되는 고급 기능으로, 타겟을 업데이트하는 방법이 어떤 의존성이 변경되었냐에 따라 달라질 때 유용합니다.

함수 (Functions)

GNU Make는 문자열 조작, 파일 탐색, 조건 평가 등을 위한 다양한 내장 함수를 제공합니다.

텍스트 처리 함수

함수 설명 예제
$(subst from,to,text) 문자열 치환 $(subst .c,.o,main.c utils.c)main.o utils.o
$(patsubst pattern,replacement,text) 패턴 치환 $(patsubst %.c,%.o,main.c)main.o
$(strip string) 앞뒤 공백 제거 $(strip foo bar )foo bar
$(findstring find,in) 부분 문자열 찾기 $(findstring a,banana)a
$(filter pattern,text) 패턴 일치 항목 필터 $(filter %.c,main.c test.o)main.c
$(filter-out pattern,text) 패턴 불일치 항목 필터 $(filter-out %.o,main.c test.o)main.c
$(sort list) 정렬 및 중복 제거 $(sort foo bar foo)bar foo
$(word n,text) n번째 단어 추출 (1-based) $(word 2,foo bar baz)bar
$(words text) 단어 개수 $(words foo bar baz)3

파일 이름 함수

함수 설명 예제
$(dir names) 디렉토리 부분 추출 $(dir src/main.c)src/
$(notdir names) 파일명 부분 추출 $(notdir src/main.c)main.c
$(suffix names) 확장자 추출 $(suffix main.c).c
$(basename names) 확장자 제거 $(basename main.c)main
$(addsuffix suffix,names) 접미사 추가 $(addsuffix .c,main utils)main.c utils.c
$(addprefix prefix,names) 접두사 추가 $(addprefix src/,main.c)src/main.c
$(join list1,list2) 두 리스트 병합 $(join a b,1 2)a1 b2
$(wildcard pattern) 파일 글로빙 $(wildcard *.c) → 현재 디렉토리의 모든 .c 파일
$(realpath names) 절대 경로 변환 $(realpath ../foo)/home/user/foo
$(abspath names) 절대 경로 (심볼릭 링크 유지) $(abspath ./foo)/current/path/foo
$(wordlist s,e,text) s번째~e번째 단어 추출 (1-based) $(wordlist 2,3,foo bar baz)bar baz
$(firstword text) 첫 번째 단어 $(firstword foo bar)foo
$(lastword text) 마지막 단어 $(lastword foo bar)bar

제어 함수

함수 설명 예제
$(foreach var,list,text) 리스트 반복 $(foreach f,$(SRCS),$(f:.c=.o))
$(if condition,then-part,else-part) 조건부 평가 $(if $(DEBUG),-g,-O2)
$(or condition1,condition2,...) 논리 OR (빈 문자열이 아닌 첫 값 반환) $(or $(VAR1),default)
$(and condition1,condition2,...) 논리 AND (모두 참이면 마지막 값, 하나라도 거짓이면 빈 문자열) $(and $(VAR1),$(VAR2))
$(call variable,param1,param2,...) 매개변수화된 함수 호출 아래 예제 참고
$(eval text) 동적 Makefile 코드 평가 아래 예제 참고
$(origin variable) 변수의 출처 확인 default, environment, file, override
$(shell command) 쉘 명령어 실행 $(shell uname -s)Linux
$(error text) 오류 발생 및 중단 $(error CC not defined)
$(warning text) 경고 메시지 출력 $(warning Deprecated option)
$(info text) 정보 메시지 출력 $(info Building $(TARGET))
$(let var [var ...],list,text) 변수를 리스트 항목에 바인딩 후 텍스트 평가 (GNU Make 4.4+) 아래 예제 참고
$(intcmp lhs,rhs[,lt[,eq[,gt]]]) 정수 비교 (GNU Make 4.4+) $(intcmp 3,5,less,equal,greater)less
$(value variable) 변수를 확장하지 않고 원본 값 반환 $(value PATH)$PATH의 원본 문자열
$(file op filename[,text]) 파일 읽기/쓰기 (GNU Make 4.0+) $(file >output.txt,$(OBJS))
$(flavor variable) 변수의 종류 반환: undefined, recursive, simple $(flavor CC)recursive

함수 사용 예제

# foreach 루프
DIRS = src tests docs
all:
	$(foreach d,$(DIRS),$(MAKE) -C $(d);)

# call을 이용한 사용자 정의 함수
reverse = $(2) $(1)
foo := $(call reverse,a,b)  # foo = "b a"

# 복잡한 함수 예제: 소스에서 오브젝트 생성
src-to-obj = $(addprefix build/,$(patsubst %.c,%.o,$(notdir $(1))))
SRCS = src/main.c src/utils.c
OBJS = $(call src-to-obj,$(SRCS))  # build/main.o build/utils.o

# eval을 이용한 동적 규칙 생성
define COMPILE_TEMPLATE
$(1): $(2)
	$(CC) $(CFLAGS) -o $$@ $$<
endef

$(eval $(call COMPILE_TEMPLATE,main.o,main.c))
$(eval $(call COMPILE_TEMPLATE,utils.o,utils.c))

고급 함수 예제

# let 함수: 지역 변수 바인딩 (GNU Make 4.4+)
# 리스트의 첫 번째와 나머지를 분리
reverse = $(let first rest,$1,\
             $(if $(rest),$(call reverse,$(rest)) )$(first))

all:
	@echo $(call reverse,d c b a)   # → "a b c d"

# intcmp 함수: 정수 비교 (GNU Make 4.4+)
JOBS ?= 4
PARALLEL_MSG = $(intcmp $(JOBS),1,parallel,single,parallel)
# JOBS=1이면 "single", 아니면 "parallel"

MIN_VER := 4
MAKE_VER := $(firstword $(subst ., ,$(MAKE_VERSION)))
VER_OK  := $(intcmp $(MAKE_VER),$(MIN_VER),,ok,ok)
ifndef VER_OK
  $(error GNU Make 4.0 이상이 필요합니다. 현재: $(MAKE_VERSION))
endif

# value 함수: 확장 없이 원본 값 조회
FOO = $$PATH    # 재귀 확장 변수 (의도적으로 $ 포함)
all:
	@echo $(FOO)           # → (PATH 환경변수 값): ATH ($ 하나는 빈 변수)
	@echo $(value FOO)     # → "$PATH" (원본 문자열)

# file 함수: 파일 읽기/쓰기 (GNU Make 4.0+)
# 명령행이 너무 길 때 응답 파일(response file) 사용
program: $(OBJECTS)
	$(file >$@.rsp,$^)   # 목록을 파일에 쓰기 (덮어쓰기)
	$(LD) $(LDFLAGS) @$@.rsp -o $@
	@rm $@.rsp

# 각 오브젝트를 별도 줄로 쓰기
program: $(OBJECTS)
	$(file >$@.rsp) $(foreach O,$^,$(file >>$@.rsp,$O))
	$(LD) @$@.rsp -o $@

# 파일 내용 읽기
VERSION := $(file <version.txt)   # version.txt 내용을 변수에

# flavor 함수: 변수 종류 확인
$(info CC flavor: $(flavor CC))        # recursive
$(info SRCS flavor: $(flavor SRCS))    # simple (SRCS := ...으로 정의됨)
$(info UNDEFINED: $(flavor UNDEF_VAR)) # undefined

조건부 처리 (Conditional Directives)

Makefile 내에서 조건에 따라 다른 규칙이나 변수를 정의할 수 있습니다.

ifdef / ifndef

# 변수 정의 여부 확인
ifdef DEBUG
  CFLAGS += -g -DDEBUG
else
  CFLAGS += -O2 -DNDEBUG
endif

# 변수가 정의되지 않았을 때
ifndef CC
  CC = gcc
endif

ifeq / ifneq

# 값 비교
ifeq ($(CC),gcc)
  CFLAGS += -fdiagnostics-color=always
endif

# 불일치 비교
ifneq ($(ARCH),x86_64)
  CROSS_COMPILE ?= aarch64-linux-gnu-
endif

# 복잡한 조건
ifeq ($(shell uname -s),Linux)
  PLATFORM = linux
  LIBS += -lpthread
else ifeq ($(shell uname -s),Darwin)
  PLATFORM = macos
  LIBS += -framework CoreFoundation
else
  $(error Unsupported platform)
endif

Makefile 지시자 (Directives)

지시자(Directive)는 Makefile 파싱 중에 Make에게 특별한 처리를 지시하는 명령입니다.

include 지시자

include 지시자는 현재 Makefile 파싱을 일시 중단하고 다른 파일을 읽어들입니다.

# 기본 include
include config.mk rules.mk

# 와일드카드와 변수 사용 가능
include $(bar) *.mk

# 파일이 없어도 에러 없이 계속 (소프트 include)
-include $(DEPS)      # 의존성 파일이 없어도 무시
sinclude optional.mk   # -include의 다른 이름

# 여러 파일 한 번에 include
include a.mk b.mk $(c).mk

# include 검색 경로 추가 (명령행)
# $ make -I/path/to/includes
# 현재 include 검색 경로 확인
$(info Include dirs: $(.INCLUDE_DIRS))
ℹ️

include 파일을 찾는 순서:

  1. 현재 디렉토리
  2. -I 옵션으로 지정된 디렉토리
  3. /usr/local/include, /usr/gnu/include, /usr/include

include한 Makefile도 재빌드 대상이 됩니다. 규칙이 있으면 자동으로 최신 버전을 다시 읽습니다.

define 지시자 (다중 라인 변수)

# 여러 줄 변수 정의
define NEWLINE


endef

# 기본값: 재귀 확장 (=)
define COMPILE_CMD
@echo "Compiling $(1) → $(2)"
$(CC) $(CFLAGS) -c -o $(2) $(1)
endef

# 즉시 확장 (:=)
define GREETING :=
Hello, $(USER)!
endef

# 조건부 할당 (?=)
define DEFAULT_FLAGS ?=
-Wall -Wextra
endef

# 추가 (+=)
define EXTRA_RECIPE +=
@echo "Additional step"
endef

undefine 지시자

# 변수 완전히 삭제 (마치 정의된 적 없는 것처럼)
TEMP_VAR = some_value
undefine TEMP_VAR
$(info flavor: $(flavor TEMP_VAR))  # → undefined

# ?= 와 undefine 비교:
# 변수를 빈 문자열로 정의: 여전히 "정의됨" → ?= 영향 없음
# undefine 후: "정의 안 됨" → ?= 가 효과를 발휘
EMPTY_VAR =
EMPTY_VAR ?= new_value   # 효과 없음 (이미 정의됨)
undefine EMPTY_VAR
EMPTY_VAR ?= new_value   # 이제 new_value가 됨

두 단계 파싱 (Two-phase Parsing)

GNU Make는 Makefile을 두 단계로 처리합니다. 이를 이해하면 변수와 함수의 확장 시점을 예측할 수 있습니다.

단계 설명 처리 대상
1단계: 읽기 (Read Phase) 모든 Makefile을 파싱하여 변수, 규칙, 의존성 그래프를 내부화 변수 정의, 규칙 헤더(타겟/의존성), 지시자
2단계: 실행 (Execute Phase) 업데이트가 필요한 타겟을 결정하고 레시피 실행 레시피, 자동 변수
Make 두 단계 파싱 타임라인 시작 종료 1단계: 읽기 (Read Phase) 모든 Makefile 파싱 · 내부 데이터베이스 구축 2단계: 실행 (Execute Phase) 타겟 갱신 필요 판단 · 레시피 실행 1단계에서 처리 (즉시 확장): 변수 정의 좌변: CC = gcc (이름 즉시) 규칙 타겟·의존성: target: dep1 조건 지시자: ifdef / ifeq include 지시자: 해당 파일 즉시 파싱 ⚠ 자동 변수($@, $<) 아직 없음! ifdef $@ 은 빈 문자열로 평가 2단계에서 처리 (지연 확장): 레시피 각 줄: $(CC) -o $@ $^ 자동 변수: $@, $<, $^, $?, $* = 재귀 확장 변수의 우변 값 셸 명령 실행: 타겟 생성 ✓ 자동 변수 사용 가능! if [ "$@" = ... ] 셸 조건문으로 처리 := (즉시) → 1단계 우변 평가 | = (지연) → 2단계 레시피 실행 시 우변 평가 | ?= / += → 1단계 적용, 값은 종류에 따름
그림: Make 두 단계 파싱 — 1단계(읽기)와 2단계(실행)에서 각각 처리되는 요소
# 확장 시점 요약

# [즉시 확장] — 1단계에서 파싱 시 즉시
IMMEDIATE = DEFERRED         # 좌변: 즉시, 우변: 지연
IMMEDIATE := IMMEDIATE        # 좌변: 즉시, 우변: 즉시
IMMEDIATE ::= IMMEDIATE       # 좌변: 즉시, 우변: 즉시
IMMEDIATE :::= IMM-ESCAPED    # 즉시 확장 후 이스케이프
IMMEDIATE += DEF/IMM          # 이전 변수 종류에 따름
IMMEDIATE != IMMEDIATE        # 우변: 즉시 (쉘 실행)

# [타겟/의존성] — 1단계에서 즉시
# IMMEDIATE : IMMEDIATE ; DEFERRED
#             DEFERRED

# [조건 지시자] — 1단계에서 즉시 파싱 (자동 변수 사용 불가!)
ifdef DEBUG        # 1단계에서 평가됨 — $@, $< 등 사용 불가
  CFLAGS += -g
endif

# 잘못된 예: 레시피에서 조건문으로 자동 변수 사용 불가
# ifdef $@         ← 불가 (빈 문자열로 확장됨)

# 레시피에서는 쉘 조건문 사용해야 함
target:
	if [ "$(ARCH)" = "arm64" ]; then echo "ARM64"; fi

VPATH와 vpath

소스 파일이 여러 디렉토리에 분산되어 있을 때, Make가 파일을 찾을 수 있도록 검색 경로를 지정합니다.

VPATH 변수

# 모든 의존성 파일을 이 디렉토리들에서 검색
VPATH = src:include:../common

program: main.o utils.o
	$(CC) -o $@ $^

# Make가 src/, include/, ../common/에서 main.c와 utils.c를 자동으로 찾음

vpath 지시자

vpath는 특정 패턴의 파일에만 검색 경로를 지정합니다.

# .c 파일은 src/에서, .h 파일은 include/에서 찾기
vpath %.c src
vpath %.h include

# 여러 디렉토리 지정 (콜론으로 구분)
vpath %.c src:../common/src

# vpath 패턴 초기화
vpath %.c  # .c 파일의 검색 경로 제거
vpath       # 모든 vpath 설정 제거

고급 기법 (Advanced Techniques)

레시피 접두사 (Recipe Prefixes)

레시피 각 줄 앞에 특수 접두사를 붙여 Make의 동작을 제어할 수 있습니다. 접두사는 조합 가능합니다.

접두사 이름 동작 예제
@ Silent 명령어 자체를 화면에 출력하지 않음 @echo "Building..."
- Ignore Error 명령어가 실패해도 계속 진행 -rm -f output.o
+ Force Run -n, -t, -q가 지정되어도 항상 실행. Jobserver 접근 허용 +$(MAKE) -C subdir
# @ : 명령어 출력 억제
all:
	@echo "=== 빌드 시작 ==="    # echo 명령어는 숨기고 결과만 출력
	@$(CC) -o program main.c    # 컴파일 명령어도 숨김

# - : 에러 무시
clean:
	-rm -f *.o program    # 파일이 없어도 에러 무시하고 계속
	-rmdir build/          # 디렉토리가 비어있지 않아도 무시

# + : 항상 실행 (-n 드라이런 중에도)
recursive:
	+$(MAKE) -C lib all   # 재귀 make는 항상 +를 사용 (jobserver 지원)

# 접두사 조합
install:
	@-mkdir -p $(DESTDIR)$(bindir)   # 조용하게, 에러 무시
	@install -m 755 $(PROGRAM) $(DESTDIR)$(bindir)
⚠️

재귀 Make에서 + 접두사 필수: $(MAKE)로 재귀 호출할 때 +를 붙이지 않으면, -n(드라이런) 모드에서 서브 디렉토리 빌드를 건너뜁니다. 또한 -j 병렬 모드에서 Jobserver에 접근하려면 반드시 +가 필요합니다.

단일 쉘 실행 (.ONESHELL)

# 기본 동작: 각 레시피 줄마다 별도의 서브 쉘 실행
bad-example:
	VAR=hello
	echo $$VAR    # 빈 문자열 출력! 다른 쉘에서 실행됨

# .ONESHELL: 모든 레시피 줄을 하나의 쉘에서 실행
.ONESHELL:
good-example:
	VAR=hello
	echo $$VAR    # "hello" 출력

# .ONESHELL 활용: 복잡한 쉘 스크립트
.ONESHELL:
deploy:
	set -e         # 에러 시 즉시 종료
	echo "배포 시작..."
	ssh server "mkdir -p /opt/app"
	scp program server:/opt/app/
	ssh server "systemctl restart app"
	echo "배포 완료"

# 쉘 선택 (.SHELLFLAGS 포함)
SHELL = /bin/bash
.SHELLFLAGS = -eu -o pipefail -c   # 파이프 에러도 감지

자동 의존성 생성

GCC의 -MMD 옵션을 사용하여 헤더 파일 의존성을 자동으로 생성하고 포함시킵니다.

# 소스 파일 목록
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)
DEPS = $(SRCS:.c=.d)

# 컴파일 시 .d 파일 자동 생성
%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP -c -o $@ $<

# 의존성 파일 포함 (존재하지 않아도 에러 안 남)
-include $(DEPS)

clean:
	rm -f $(OBJS) $(DEPS) program
ℹ️

-MP 옵션은 각 헤더 파일에 대해 빈 타겟을 생성하여, 헤더 파일이 삭제되었을 때 Make 에러를 방지합니다.

조용한 빌드 (Silent Build)

# @ 접두사: 명령어 출력 억제
all:
	@echo "Building project..."
	@$(MAKE) program

# .SILENT 특수 타겟: 전체 또는 특정 타겟 출력 억제
.SILENT: clean

clean:
	rm -f *.o program  # 명령어가 출력되지 않음

# 전역 조용한 모드
.SILENT:

병렬 빌드

Make는 -j 옵션으로 독립적인 타겟을 병렬로 빌드합니다.

# 최대 4개 작업 병렬 실행
$ make -j4

# CPU 코어 수만큼 병렬 실행
$ make -j$(nproc)

# Makefile 내에서 병렬 제어
.NOTPARALLEL: install  # install 타겟은 순차 실행

# 순서 의존성 명시
install: program
	@echo "Installing..."

# program이 완료된 후에만 install 실행됨
Make 병렬 빌드 Gantt 차트 비교 make -j1 (순차) vs make -j4 (병렬) 빌드 시간 비교 0s 1s 2s 3s 4s 5s 6s make -j1 (순차 실행) Worker main.o (1s) utils.o (1s) parser.o (1s) helper.o (1s) program (1s) 총 5초 make -j4 (병렬 실행) Worker 1 main.o (1s) program (1s) Worker 2 utils.o (1s) 유휴 Worker 3 parser.o (1s) 유휴 Worker 4 helper.o (1s) 유휴 총 2초 (2.5배 빠름!) ※ program은 모든 .o 파일에 의존 → .o들이 모두 완료된 후 시작
그림: make -j4 병렬 빌드 — 독립적인 .o 파일들을 동시에 컴파일하여 빌드 시간 단축

특수 내장 변수 (Special Variables)

GNU Make는 빌드 환경을 제어하는 특수 변수를 자동으로 제공합니다.

변수 설명 예시
MAKE 현재 실행 중인 Make 프로그램 경로. 재귀 호출 시 반드시 사용 $(MAKE) -C subdir
MAKEFILE_LIST 읽어들인 Makefile들의 목록 (include 포함) $(lastword $(MAKEFILE_LIST))
MAKEFLAGS 현재 Make에 전달된 플래그. 재귀 호출 시 자동 전달 $(MAKEFLAGS)-j4
MAKECMDGOALS 명령행에서 지정된 타겟 목록 ifeq ($(MAKECMDGOALS),clean)
MAKELEVEL 재귀 Make 호출 깊이 (최상위=0) 재귀 깊이 제한에 사용
MAKE_VERSION GNU Make 버전 문자열 4.4.1
MAKE_HOST Make가 실행되는 호스트 시스템 x86_64-pc-linux-gnu
.DEFAULT_GOAL 인자 없이 make 실행 시 사용할 기본 타겟 .DEFAULT_GOAL := all
.RECIPEPREFIX 레시피 접두 문자 변경 (기본: 탭) .RECIPEPREFIX := >
.INCLUDE_DIRS include 검색 경로 목록 읽기 전용
.LOADED 동적으로 로드된 오브젝트 목록 읽기 전용
CURDIR 현재 작업 디렉토리 절대 경로 (-C 적용 후) $(CURDIR)
# 현재 Makefile의 디렉토리 경로 (include 된 Makefile에서 유용)
THIS_MAKEFILE := $(lastword $(MAKEFILE_LIST))
THIS_DIR      := $(dir $(abspath $(THIS_MAKEFILE)))

# 명령행 타겟에 따른 조건부 처리
ifeq ($(MAKECMDGOALS),clean)
  # clean 중에는 의존성 파일 읽지 않음
else
  -include $(DEPS)
endif

# 탭 대신 다른 문자로 레시피 구분
.RECIPEPREFIX = >
all:
> echo "탭 대신 > 사용"    # Makefile 보기 좋아짐

재귀 Make

대규모 프로젝트에서는 각 서브디렉토리마다 Makefile을 두고 재귀적으로 호출합니다.

# 최상위 Makefile
SUBDIRS = lib src tests

all:
	@for dir in $(SUBDIRS); do \
		$(MAKE) -C $$dir all || exit 1; \
	done

# 또는 foreach 사용
all:
	$(foreach dir,$(SUBDIRS),$(MAKE) -C $(dir) all &&) true
Make 재귀 호출 구조 트리 Makefile (루트) MAKELEVEL=0 /project/Makefile $(MAKE) -C lib $(MAKE) -C src $(MAKE) -C tests lib/Makefile MAKELEVEL=1 변수 자동 상속: CC, CFLAGS src/Makefile MAKELEVEL=1 MAKEFLAGS 자동 전달 (-j4 등) tests/Makefile MAKELEVEL=1 lib에 의존 → 순서 보장 필요 $(MAKE) -C core $(MAKE) -C ui src/core/Makefile MAKELEVEL=2 src/ui/Makefile MAKELEVEL=2 재귀 Make 핵심 규칙 • 하위 Make 호출 시 반드시 $(MAKE) 사용 (make 직접 호출 금지 — -j 플래그 미전달) • MAKEFLAGS, CC, CFLAGS 등 export된 변수는 자동 전달 • MAKELEVEL 로 재귀 깊이 확인 가능
그림: 재귀 Make 호출 구조 — MAKELEVEL로 재귀 깊이 추적, $(MAKE) 사용이 필수
⚠️

재귀 Make의 문제점: Peter Miller의 논문 "Recursive Make Considered Harmful"에서 지적했듯이, 재귀 Make는 의존성 추적이 불완전하고 병렬 빌드 효율이 떨어집니다. 가능하면 단일 Makefile에 모든 규칙을 포함하거나, 비재귀 패턴을 사용하세요.

다중 라인 변수 (define)

# 여러 줄의 레시피를 변수로 정의
define COMPILE_RULE
@echo "Compiling $(1)..."
$(CC) $(CFLAGS) -c -o $(1) $(2)
@echo "Done."
endef

main.o: main.c
	$(call COMPILE_RULE,$@,$<)

이중 확장 (Secondary Expansion)

.SECONDEXPANSION을 사용하면 의존성을 두 번 확장하여 동적 의존성을 구현할 수 있습니다.

.SECONDEXPANSION:

SRCS_main = main.c utils.c
SRCS_test = test.c utils.c

# $$ 는 두 번째 확장 시 $ 로 평가됨
%: $$(SRCS_%)
	$(CC) -o $@ $^

# make main 실행 시 SRCS_main이 의존성으로 확장됨

크로스 컴파일 & 커널 빌드

Linux 커널과 임베디드 시스템 개발에서는 호스트 머신과 다른 아키텍처를 위한 크로스 컴파일이 필수입니다. GNU Make는 변수를 통해 크로스 컴파일을 쉽게 지원합니다.

커널 표준 변수

Linux 커널 빌드 시스템(Kbuild)은 다음 표준 변수를 사용합니다:

변수 설명 예제
ARCH 타겟 아키텍처 ARCH=arm64, ARCH=x86
CROSS_COMPILE 크로스 컴파일러 접두사 CROSS_COMPILE=aarch64-linux-gnu-
O 출력 디렉토리 (out-of-tree build) O=build, O=/tmp/kernel-build
M 외부 모듈 소스 디렉토리 M=/home/user/mymodule
CC C 컴파일러 (보통 자동 설정) CC=clang
LD 링커 LD=ld.lld

크로스 컴파일 예제

# ARM64 커널 빌드
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)

# RISC-V 커널 빌드
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- Image -j8

# Out-of-tree 빌드 (소스 트리를 깨끗하게 유지)
$ make O=../build-arm64 ARCH=arm64 defconfig
$ make O=../build-arm64 ARCH=arm64 -j$(nproc)

# 외부 커널 모듈 빌드
$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules_install

# Clang으로 커널 빌드 (LLVM 툴체인)
$ make CC=clang LD=ld.lld AR=llvm-ar NM=llvm-nm OBJCOPY=llvm-objcopy -j$(nproc)

# 또는 LLVM=1 단축 옵션 (커널 5.7+)
$ make LLVM=1 -j$(nproc)

외부 커널 모듈 Makefile 예제

# 외부 모듈용 Makefile (/home/user/mydriver/Makefile)
obj-m := mydriver.o
mydriver-objs := main.o ioctl.o interrupt.o

# 다중 파일 소스
ccflags-y := -DDEBUG -I$(src)/include

# 커널 소스 트리 경로
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

install:
	$(MAKE) -C $(KDIR) M=$(PWD) modules_install

.PHONY: all clean install

일반 프로젝트 크로스 컴파일

# 범용 크로스 컴파일 Makefile
CROSS_COMPILE ?=
CC := $(CROSS_COMPILE)gcc
AR := $(CROSS_COMPILE)ar
LD := $(CROSS_COMPILE)ld
STRIP := $(CROSS_COMPILE)strip

# 타겟별 플래그
ifeq ($(ARCH),arm)
  CFLAGS += -march=armv7-a -mfpu=neon
else ifeq ($(ARCH),arm64)
  CFLAGS += -march=armv8-a
else ifeq ($(ARCH),riscv)
  CFLAGS += -march=rv64gc -mabi=lp64d
endif

program: $(OBJS)
	$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
	$(STRIP) $@

# 사용 예:
# make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
💡

Out-of-tree 빌드의 장점: O= 변수를 사용하면 소스 트리에 빌드 산출물이 섞이지 않아, 동일한 소스로 여러 아키텍처를 동시에 빌드하거나 git status가 깨끗하게 유지됩니다. 커널 개발 시 필수 패턴입니다.

성능 최적화

대규모 프로젝트에서는 빌드 시간이 개발 생산성에 직접적인 영향을 미칩니다. 다음 기법으로 Make 빌드를 최적화할 수 있습니다.

ccache — 컴파일 캐시

ccache는 컴파일 결과를 캐시하여 이전에 빌드한 적이 있는 파일은 재컴파일하지 않습니다. 커널 개발처럼 자주 make clean하는 경우 매우 효과적입니다.

# ccache 설치
$ sudo apt install ccache    # Debian/Ubuntu
$ sudo dnf install ccache    # Fedora/RHEL

# Makefile에 통합
CC := ccache gcc
CXX := ccache g++

# 또는 환경 변수로 전역 활성화
$ export CC="ccache gcc"
$ export CXX="ccache g++"

# 커널 빌드 시
$ make CC="ccache gcc" -j$(nproc)

# 캐시 통계 확인
$ ccache -s
ℹ️

ccache는 첫 빌드는 느리지만, 두 번째 빌드부터는 5-10배 빨라집니다. 캐시 크기는 기본 5GB이며, ccache -M 10G로 늘릴 수 있습니다.

distcc — 분산 컴파일

distcc는 컴파일 작업을 네트워크상의 여러 머신에 분산시킵니다. 팀 개발 환경에서 유휴 머신을 빌드 서버로 활용할 수 있습니다.

# distcc 서버 설정 (빌드 서버)
$ sudo apt install distcc
$ distccd --daemon --allow 192.168.1.0/24

# 클라이언트 설정 (개발 머신)
$ export DISTCC_HOSTS="localhost 192.168.1.10 192.168.1.11"
$ export CC="distcc gcc"

# 병렬도를 머신 수 * 코어 수로 설정
$ make -j20 CC="distcc gcc"

# distcc 통계 확인
$ distccmon-text 1

병렬 빌드 최적화

-j 옵션의 최적 값은 CPU 코어 수와 작업 특성에 따라 다릅니다.

# CPU 코어 수만큼 (일반적인 선택)
$ make -j$(nproc)

# I/O 바운드 작업은 코어 수의 1.5-2배
$ make -j$(($(nproc) * 2))

# ccache와 함께 사용 시 더 높은 병렬도 허용
$ make -j$(($(nproc) * 3)) CC="ccache gcc"

# 메모리가 부족하면 병렬도 낮추기
$ make -j4  # 링크 단계에서 OOM 방지

# Makefile에서 기본 병렬도 설정 (GNU Make 4.3+)
MAKEFLAGS += -j$(shell nproc)
⚠️

메모리 부족 주의: 링크 단계는 메모리를 많이 사용합니다. 대규모 C++ 프로젝트에서 -j$(nproc)를 사용하면 OOM Killer가 발동할 수 있으므로, 링크 단계는 병렬도를 낮추는 것이 좋습니다.

의존성 최적화

불필요한 의존성은 병렬성을 저해하고 빌드 시간을 늘립니다.

# 잘못된 예: 모든 파일이 common.h에 의존
OBJS = main.o utils.o parser.o network.o
$(OBJS): common.h  # common.h 수정 시 모두 재컴파일

# 올바른 예: 실제 의존성만 선언
main.o: main.c common.h
utils.o: utils.c common.h
parser.o: parser.c        # common.h 불필요
network.o: network.c      # common.h 불필요

# 더 나은 방법: -MMD로 자동 의존성 생성
%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP -c $<

-include $(DEPS)

증분 빌드 전략

# 링크 시간 최적화(LTO) 비활성화 (개발 중)
ifdef DEBUG
  CFLAGS += -g -O0
else
  CFLAGS += -O2 -flto  # 릴리스만 LTO
endif

# 미리 컴파일된 헤더 (PCH) 사용 (대규모 C++ 프로젝트)
pch.h.gch: pch.h
	$(CXX) $(CXXFLAGS) -x c++-header $< -o $@

%.o: %.cpp pch.h.gch
	$(CXX) $(CXXFLAGS) -include pch.h -c $<

# Unity build (모든 소스를 하나의 컴파일 단위로)
# 빌드는 빠르지만 증분 빌드 불가, 릴리스 전용
all_sources.o: $(SRCS)
	echo '$(SRCS)' | tr ' ' '\n' | sed 's/^/#include "/' | sed 's/$$/"/' > all.cpp
	$(CXX) $(CXXFLAGS) -c all.cpp -o $@

빌드 시간 프로파일링

# Make가 수행한 작업 시간 측정
$ make --debug=v 2>&1 | grep "Considering"

# 각 명령어 실행 시간 측정
$ time make -j$(nproc)

# 병렬 빌드 시각화 (GNU Make 4.0+)
$ make -j8 --output-sync=target

# 커널 빌드 시간 상세 분석
$ scripts/build-time.sh  # 커널 소스 트리 내

# Makefile에 타이머 추가
BUILD_START := $(shell date +%s)
all: $(TARGET)
	@echo "Build completed in $$(($$(date +%s) - $(BUILD_START))) seconds"
최적화 기법 적용 시점 속도 향상 단점
-j$(nproc) 항상 2-8배 메모리 사용량 증가
ccache 반복 빌드 5-10배 디스크 공간 5-10GB
distcc 팀 개발 머신 수만큼 네트워크 설정 필요
-MMD 자동 의존성 항상 10-20% 없음
Unity build CI/릴리스 30-50% 증분 빌드 불가

특수 타겟 (Special Targets)

GNU Make는 빌드 동작을 제어하는 특수한 내장 타겟을 제공합니다.

특수 타겟 설명
.PHONY 파일이 아닌 논리적 타겟 선언 (예: clean, all)
.DEFAULT 규칙이 없는 타겟의 기본 레시피 정의
.IGNORE 에러를 무시하고 계속 실행 (-i 옵션과 동일)
.SILENT 명령어 출력 억제 (-s 옵션과 동일)
.DELETE_ON_ERROR 레시피 실패 시 타겟 파일 자동 삭제
.NOTPARALLEL 병렬 실행 비활성화
.ONESHELL 모든 레시피 라인을 하나의 쉘에서 실행
.POSIX POSIX 호환 모드 활성화
.SUFFIXES 접미사 규칙 정의 (구식, 패턴 규칙 권장)
.PRECIOUS 중간 파일을 빌드 후 삭제하지 않고 보존
.INTERMEDIATE 중간 파일로 선언 (빌드 후 자동 삭제)
.SECONDARY 중간 파일이지만 삭제하지 않음
.SECONDEXPANSION 의존성의 이중 확장 활성화
# PHONY 타겟 선언 (실제 파일 이름과 충돌 방지)
.PHONY: all clean install test

# 에러 발생 시 생성된 파일 자동 삭제
.DELETE_ON_ERROR:

# 모든 레시피를 하나의 쉘에서 실행 (변수 공유 가능)
.ONESHELL:
test:
	VAR=hello
	echo $$VAR  # .ONESHELL 없으면 빈 문자열 출력

암묵적 규칙 목록 (Built-in Implicit Rules)

GNU Make는 일반적인 파일 변환을 위한 내장 암묵적 규칙을 제공합니다. 확장자로 자동 선택되며, 관련 변수를 재정의하면 동작을 변경할 수 있습니다.

내장 암묵적 규칙 목록

목표 소스 레시피 관련 변수
%.o %.c $(CC) $(CPPFLAGS) $(CFLAGS) -c CC, CFLAGS, CPPFLAGS
%.o %.cc / %.cpp $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c CXX, CXXFLAGS
%.o %.s $(AS) $(ASFLAGS) AS, ASFLAGS
%.o %.S $(CC) $(CPPFLAGS) -c (전처리 후 어셈블) CC, CPPFLAGS
% (실행 파일) %.o $(CC) $(LDFLAGS) %.o $(LDLIBS) CC, LDFLAGS, LDLIBS
%.c %.y $(YACC) $(YFLAGS) YACC, YFLAGS
%.c %.l $(LEX) $(LFLAGS) LEX, LFLAGS
%.a %.o $(AR) $(ARFLAGS) $@ $< AR, ARFLAGS

암묵적 규칙에서 사용하는 표준 변수

변수 기본값 용도
CC cc C 컴파일러
CXX g++ C++ 컴파일러
AS as 어셈블러
AR ar 정적 라이브러리 아카이버
RANLIB ranlib 라이브러리 인덱스 생성
YACC yacc Yacc 파서 생성기
LEX lex Lex 어휘 분석기 생성기
RM rm -f 파일 삭제
CFLAGS (없음) C 컴파일 플래그
CXXFLAGS (없음) C++ 컴파일 플래그
CPPFLAGS (없음) 전처리기 플래그 (-I, -D)
LDFLAGS (없음) 링커 플래그 (-L)
LDLIBS (없음) 링크할 라이브러리 (-l)
ARFLAGS rv 아카이브 플래그
# 암묵적 규칙 활용 예: 최소한의 Makefile
CC     = gcc
CFLAGS = -Wall -O2
LDLIBS = -lm

# 명시적 규칙 없이도 program: main.o utils.o 후
# Make가 자동으로 %.o: %.c 암묵적 규칙 적용
program: main.o utils.o

# 암묵적 규칙 비활성화: 명시적 빈 규칙으로
%.o: %.s    # 어셈블러 암묵적 규칙 비활성화

# 또는 전역 비활성화 (성능 향상)
# $ make -r   (--no-builtin-rules)

# 라이브러리 링크: -lNAME 형식
# Make가 libNAME.so, libNAME.a 를 자동 탐색
foo: foo.c -lcurses
	$(CC) $^ -o $@
# → cc foo.c /usr/lib/libcurses.a -o foo

# 암묵적 규칙 체인 (chaining)
# foo.o → foo.c 없지만 foo.y 있으면: foo.y → foo.c → foo.o 자동 체인

Make는 타겟에 맞는 암묵적 규칙을 다음 순서로 탐색합니다:

  1. Makefile에 정의된 패턴 규칙 (사용자 정의)
  2. 내장 암묵적 규칙 (Built-in)
  3. 접미사 규칙 (Suffix rules, 구식)
  4. 마지막 수단 기본 규칙 (.DEFAULT)
# 암묵적 규칙 탐색 디버깅
$ make --debug=i target   # -i: implicit rules 탐색 과정 출력

# 모든 규칙과 변수 출력
$ make -p | less

# 내장 규칙 없이 (더 빠른 빌드, 규칙을 모두 명시)
$ make -r -R   # --no-builtin-rules --no-builtin-variables

디버깅 (Debugging)

유용한 Make 옵션

옵션 설명
-n, --dry-run 명령어를 실행하지 않고 출력만 함 (시뮬레이션)
-p, --print-data-base 모든 규칙과 변수를 출력
-d 디버그 정보 출력 (매우 상세함)
--debug=b 기본 디버그 정보 (타겟과 의존성)
--debug=v 상세 디버그 정보 (암묵적 규칙 탐색 과정)
--debug=m Makefile 리메이크 과정 디버깅
--trace 실행되는 각 레시피의 위치 출력
-B, --always-make 모든 타겟을 무조건 다시 빌드
-W file 특정 파일이 변경되었다고 가정하고 빌드
-o file 특정 파일을 최신 상태로 가정 (재빌드하지 않음)
-t 파일을 실제로 빌드하지 않고 touch만 (타임스탬프 갱신)
-q 빌드 필요 여부만 확인 (0=최신, 1=빌드필요, 2=에러)
-k 에러 발생 후에도 가능한 한 계속 빌드
-l N 시스템 로드가 N 이상이면 새 병렬 작업 시작 안 함
-r 내장 암묵적 규칙 비활성화
-R 내장 변수 비활성화 (암묵적으로 -r 포함)
-e 환경 변수가 Makefile 정의보다 우선
-s, --silent 레시피 명령어 출력 억제
-O[type] 병렬 빌드 출력 동기화 (target, line, recurse, none)
--shuffle 의존성 순서를 무작위로 섞어 순서 의존 버그 발견 (GNU Make 4.4+)
--warn-undefined-variables 미정의 변수 참조 시 경고 출력
-E STRING STRING을 Makefile 문법으로 평가 (eval 함수의 명령행 버전)

--debug 상세 레벨

옵션 설명
--debug=b (basic) 최신이 아닌 타겟과 빌드 성공 여부
--debug=v (verbose) 파싱한 Makefile, 재빌드 불필요한 의존성 포함 (basic 포함)
--debug=i (implicit) 각 타겟의 암묵적 규칙 탐색 과정 (basic 포함)
--debug=j (jobs) 서브 프로세스 호출 상세 정보
--debug=m (makefile) Makefile 재빌드 과정 (basic 포함)
--debug=p (print) .SILENT이나 @에 숨겨진 레시피도 출력, 정의 위치 표시
--debug=w (why) 각 타겟이 왜 재빌드되어야 하는지 설명
--debug=a (all) 모든 디버그 정보 (-d와 동일)
--debug=n (none) 현재 활성화된 디버그 모두 비활성화

디버깅 기법

# 변수 값 확인
$(info SRCS = $(SRCS))
$(info OBJS = $(OBJS))

# 변수 출처 확인
$(info CC origin: $(origin CC))

# 조건부 디버깅
ifdef DEBUG_MAKEFILE
  $(info Building target $@)
  $(info Dependencies: $^)
endif

# 함수 결과 확인
$(info wildcard *.c = $(wildcard *.c))

# 특정 타겟만 trace
$ make --trace main.o

자주 발생하는 문제

⚠️
  • 탭 vs 공백: 레시피는 반드시 탭 문자로 시작해야 합니다. 공백 8개가 아닙니다!
  • 재귀 확장 무한 루프: CFLAGS = $(CFLAGS) -Wall은 무한 루프를 일으킵니다. := 또는 +=를 사용하세요.
  • 쉘 변수 vs Make 변수: $(VAR)는 Make 변수, $$VAR는 쉘 변수입니다.
  • VPATH 경로 문제: VPATH로 찾은 파일의 경로는 레시피에서 $<로 접근해야 정확합니다.

빌드 오류 진단 가이드

Make 빌드 오류는 다양한 원인으로 발생합니다. 아래 가이드는 빠른 문제 해결을 돕습니다.

오류 메시지 원인 해결 방법
*** missing separator 레시피가 탭이 아닌 공백으로 시작 에디터 설정 확인, cat -A Makefile로 탭(^I) 확인
*** No rule to make target 의존성 파일이 없고 생성 규칙도 없음 파일명 오타 확인, VPATH 설정, 규칙 추가
*** Recursive variable references itself VAR = $(VAR) ... 무한 루프 := 또는 += 사용
*** unterminated call to function 함수 호출 괄호 불일치 $() 괄호 쌍 확인
*** multiple target patterns 패턴 규칙에 % 여러 개 사용 패턴 하나만 사용, 정적 패턴 규칙 검토
make: *** [target] Error 2 레시피 명령어가 비정상 종료 명령어 직접 실행하여 원인 파악, -k로 계속 진행
Nothing to be done for 'all' 모든 타겟이 최신 상태 정상 (의도치 않으면 의존성 확인, make -B로 강제 재빌드)
warning: overriding recipe for target 동일 타겟에 대한 규칙 중복 중복 규칙 제거, 더블 콜론 규칙(::) 사용 검토
💡

단계별 디버깅 전략:

  1. 문법 검증: make -n으로 실행 없이 파싱만 확인
  2. 변수 확인: make -p | grep VAR_NAME으로 최종 값 확인
  3. 규칙 추적: make --debug=v target로 규칙 매칭 과정 확인
  4. 의존성 시각화: make -nd | makefile2graph | dot -Tpng -o deps.png
  5. 단순화: 최소 Makefile로 문제 재현, 점진적으로 복잡도 증가
  6. 버전 확인: make --version (GNU Make 3.82+ 권장)
증상 진단 명령어 확인 사항
타겟이 항상 재빌드됨 make -d target 2>&1 | grep newer 의존성 타임스탬프, PHONY 타겟 여부
병렬 빌드 실패 make -j1로 순차 테스트 의존성 누락, 레이스 조건
변수 값이 예상과 다름 $(info VAR=$(VAR)) 삽입 할당 순서, 환경 변수 충돌
규칙이 매칭 안 됨 make --debug=a target 패턴 규칙, VPATH, 암묵적 규칙
빌드 속도 저하 time make -j$(nproc) I/O 병목, shell 함수 남용

Makefile 작성 관례 (Makefile Conventions)

GNU Coding Standards와 POSIX 표준은 이식성과 일관성을 위한 Makefile 작성 관례를 정의합니다. 이러한 관례를 따르면 사용자가 직관적으로 프로젝트를 빌드하고 설치할 수 있습니다.

표준 타겟 (Standard Targets)

대부분의 프로젝트는 다음 표준 타겟을 제공해야 합니다:

타겟 설명 의존성
all 기본 타겟. 모든 실행 파일과 라이브러리 빌드 없음 (최상위 타겟)
install 빌드 결과물을 시스템에 설치 all
uninstall 설치된 파일 제거 없음
clean 빌드 결과물 삭제 (오브젝트 파일, 실행 파일 등) 없음
distclean clean + 설정 파일 삭제 (Makefile, config.h 등) 없음
mostlyclean 빌드 중간 파일만 삭제 (재빌드가 빠름) 없음
check / test 테스트 실행 all
dist 배포용 tarball 생성 distclean
install-strip 디버그 심볼 제거 후 설치 all
tags / TAGS ctags 또는 etags 파일 생성 없음
# 표준 타겟 예제
.PHONY: all install uninstall clean distclean check

all: $(PROGRAMS) $(LIBRARIES)

install: all
	install -d $(DESTDIR)$(bindir)
	install -m 755 $(PROGRAMS) $(DESTDIR)$(bindir)

clean:
	rm -f *.o $(PROGRAMS)

distclean: clean
	rm -f Makefile config.h

check: all
	./run-tests.sh

표준 변수 (Standard Variables)

GNU Coding Standards는 일관된 변수명을 권장합니다. 대문자로 작성하며, 사용자가 명령행에서 재정의할 수 있어야 합니다.

컴파일러 및 도구

변수 설명 기본값 예시
CC C 컴파일러 gcc
CXX C++ 컴파일러 g++
AS 어셈블러 as
AR 아카이브 생성 도구 ar
RANLIB 아카이브 인덱스 생성 ranlib
LD 링커 ld
LEX Lexer 생성기 flex
YACC Parser 생성기 bison -y

컴파일 플래그

변수 설명 예시
CFLAGS C 컴파일 옵션 -Wall -O2
CXXFLAGS C++ 컴파일 옵션 -Wall -std=c++17
CPPFLAGS 전처리기 옵션 (헤더 경로, 매크로 정의) -Iinclude -DDEBUG
LDFLAGS 링커 옵션 (라이브러리 경로) -L/usr/local/lib
LDLIBS 링크할 라이브러리 -lm -lpthread
ARFLAGS 아카이브 옵션 rcs
ℹ️

CPPFLAGS vs CFLAGS: CPPFLAGS는 전처리기 옵션 (-I, -D)이며, C와 C++ 모두에 적용됩니다. CFLAGS는 C 컴파일러 전용 옵션입니다.

LDFLAGS vs LDLIBS: LDFLAGS는 링커 옵션 (-L, -Wl,...), LDLIBS는 라이브러리 지정 (-l...)입니다. 링커 명령행에서 LDFLAGS가 오브젝트 파일 앞에, LDLIBS는 뒤에 위치해야 합니다.

# 표준 변수 사용 예제
CC       ?= gcc
CFLAGS   ?= -Wall -O2
CPPFLAGS ?= -Iinclude
LDFLAGS  ?=
LDLIBS   ?= -lm

# 올바른 링크 명령 순서
program: main.o utils.o
	$(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)

# 올바른 컴파일 명령
%.o: %.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

디렉토리 변수 (Directory Variables)

설치 경로를 제어하는 표준 변수들입니다. GNU Autotools (autoconf/automake)와 호환되도록 작성하세요.

변수 설명 기본값
prefix 설치 루트 경로 /usr/local
exec_prefix 실행 파일 계층 루트 $(prefix)
bindir 사용자 실행 파일 $(exec_prefix)/bin
sbindir 시스템 관리자 실행 파일 $(exec_prefix)/sbin
libdir 라이브러리 $(exec_prefix)/lib
includedir 헤더 파일 $(prefix)/include
datarootdir 읽기 전용 아키텍처 독립 데이터 루트 $(prefix)/share
datadir 읽기 전용 데이터 $(datarootdir)
mandir man 페이지 $(datarootdir)/man
infodir Info 문서 $(datarootdir)/info
docdir 문서 $(datarootdir)/doc/$(PACKAGE)
sysconfdir 시스템 설정 파일 $(prefix)/etc
localstatedir 가변 데이터 $(prefix)/var
# 디렉토리 변수 정의 예제
prefix      ?= /usr/local
exec_prefix ?= $(prefix)
bindir      ?= $(exec_prefix)/bin
libdir      ?= $(exec_prefix)/lib
includedir  ?= $(prefix)/include
datarootdir ?= $(prefix)/share
mandir      ?= $(datarootdir)/man

# DESTDIR 지원 (staged install)
install: all
	install -d $(DESTDIR)$(bindir)
	install -m 755 $(PROGRAM) $(DESTDIR)$(bindir)
	install -d $(DESTDIR)$(mandir)/man1
	install -m 644 doc/$(PROGRAM).1 $(DESTDIR)$(mandir)/man1

# 사용 예: make install DESTDIR=/tmp/staging prefix=/usr
ℹ️

DESTDIR의 역할: DESTDIR는 staged install을 위한 변수입니다. 패키징 시스템 (rpm, deb)에서 파일을 임시 디렉토리에 설치한 후 패키지로 묶을 때 사용됩니다. 실제 설치 경로는 $(DESTDIR)$(prefix)/... 형태가 됩니다.

예: make install DESTDIR=/tmp/pkg prefix=/usr/tmp/pkg/usr/bin에 설치합니다.

파일명 관례

⚠️

우선 순위: make 명령은 다음 순서로 파일을 찾습니다: GNUmakefilemakefileMakefile. 이식성을 위해 Makefile 사용을 권장합니다.

코딩 관례

들여쓰기

변수명

PHONY 타겟

.PHONY: all clean install test help

all: $(TARGET)

clean:
	rm -f *.o $(TARGET)

기타 권장 사항

크로스 컴파일 지원

ARM, MIPS 등 다른 아키텍처를 대상으로 빌드할 때 사용하는 관례입니다.

# 크로스 컴파일러 접두사
CROSS_COMPILE ?=

CC  := $(CROSS_COMPILE)gcc
AR  := $(CROSS_COMPILE)ar
LD  := $(CROSS_COMPILE)ld

# 사용 예
# make CROSS_COMPILE=arm-linux-gnueabihf-
# → CC=arm-linux-gnueabihf-gcc

Linux 커널도 동일한 방식을 사용합니다:

$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

Help 타겟 추가

사용자 편의를 위해 make help 타겟을 제공하는 것이 좋습니다.

.PHONY: help
help:
	@echo "사용 가능한 타겟:"
	@echo "  all          - 전체 빌드 (기본)"
	@echo "  clean        - 빌드 결과물 삭제"
	@echo "  install      - 시스템에 설치"
	@echo "  test         - 테스트 실행"
	@echo ""
	@echo "변수:"
	@echo "  CC           - C 컴파일러 (기본: gcc)"
	@echo "  CFLAGS       - 컴파일 옵션 (기본: -Wall -O2)"
	@echo "  prefix       - 설치 경로 (기본: /usr/local)"
	@echo ""
	@echo "예: make install prefix=/opt/myapp"

또는 자동으로 주석에서 추출하는 방법:

.PHONY: help
help: ## 도움말 출력
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2}'

all: ## 전체 빌드
	...

clean: ## 빌드 결과물 삭제
	...

실전 패턴 및 모범 사례

프로젝트 구조 예제

# 디렉토리 구조
# project/
#   ├── Makefile
#   ├── src/
#   ├── include/
#   ├── build/
#   └── lib/

# === 변수 정의 ===
CC       := gcc
CFLAGS   := -Wall -Wextra -std=c11
CPPFLAGS := -Iinclude
LDFLAGS  := -Llib
LDLIBS   := -lm

# 빌드 설정
BUILD_DIR := build
SRC_DIR   := src
INC_DIR   := include

# === 소스 파일 수집 ===
SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))
DEPS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.d,$(SRCS))

TARGET := $(BUILD_DIR)/program

# === 디버그/릴리스 모드 ===
ifdef DEBUG
  CFLAGS += -g -O0 -DDEBUG
else
  CFLAGS += -O2 -DNDEBUG
endif

# === 기본 타겟 ===
.PHONY: all clean install test

all: $(TARGET)

# === 링크 ===
$(TARGET): $(OBJS)
	@mkdir -p $(dir $@)
	$(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)
	@echo "Build complete: $@"

# === 컴파일 ===
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(dir $@)
	$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c -o $@ $<

# === 의존성 포함 ===
-include $(DEPS)

# === 정리 ===
clean:
	rm -rf $(BUILD_DIR)

# === 설치 ===
PREFIX ?= /usr/local
install: $(TARGET)
	install -D -m 755 $(TARGET) $(PREFIX)/bin/$(notdir $(TARGET))

# === 디버그 정보 출력 ===
print-%:
	@echo '$*=$($*)'

# 사용: make print-SRCS

모범 사례 및 팁

빠른 참조 (Quick Reference)

자동 변수 전체 목록

변수 D/F 변형 설명
$@ $(@D) / $(@F) 타겟 이름 / 타겟의 디렉토리 / 타겟의 파일명
$% $(%D) / $(%F) 아카이브 멤버명 (lib(member.o)에서 member.o)
$< $(<D) / $(<F) 첫 번째 의존성
$^ $(^D) / $(^F) 모든 의존성 (중복 제거)
$+ $(+D) / $(+F) 모든 의존성 (중복 포함, 원래 순서)
$? $(?D) / $(?F) 타겟보다 새로운 의존성만
$* $(*D) / $(*F) 패턴 규칙의 스템(%.c: %.o에서 베이스명)

함수 빠른 참조

카테고리 함수 설명
문자열 $(subst from,to,text) 문자열 치환
$(patsubst pat,rep,text) 패턴 치환
$(strip text) 앞뒤 공백 제거
$(findstring find,in) 부분 문자열 검색
$(filter pat,text) 패턴 일치 필터
$(filter-out pat,text) 패턴 불일치 필터
$(sort list) 정렬 및 중복 제거
$(word n,text) n번째 단어
$(words text) 단어 수
파일명 $(dir names) 디렉토리 부분
$(notdir names) 파일명 부분
$(suffix names) 확장자
$(basename names) 확장자 제거
$(addsuffix suf,names) 접미사 추가
$(addprefix pre,names) 접두사 추가
$(wildcard pat) 파일 글로빙
$(realpath names) 절대 경로 (심볼릭 링크 해석)
제어 $(foreach var,list,text) 리스트 반복
$(if cond,then[,else]) 조건 선택
$(or c1[,c2...]) 논리 OR
$(and c1[,c2...]) 논리 AND
$(call var,p1,p2,...) 사용자 함수 호출
$(eval text) 동적 Makefile 생성
$(shell cmd) 쉘 명령 실행
$(error msg) / $(warning msg) 메시지 출력/중단
정보 $(origin var) 변수 출처 (file/environment/…)
$(flavor var) 변수 종류 (recursive/simple/…)
$(value var) 확장 없이 원본 값
$(file op name[,text]) 파일 읽기/쓰기

자주 쓰는 make 옵션

옵션 설명 사용 예
-j N N개 작업 병렬 실행 make -j$(nproc)
-f file 사용할 Makefile 지정 make -f build.mk
-C dir 디렉토리 변경 후 실행 make -C src all
-n 드라이런 (실행하지 않고 출력만) make -n install
-B 모든 타겟 강제 재빌드 make -B
-k 에러 후에도 계속 빌드 make -k all
-p 모든 규칙/변수 출력 make -p -f /dev/null
-d 전체 디버그 정보 make -d target 2>&1 | less
VAR=val 변수 오버라이드 make CC=clang CFLAGS=-O0
-s 조용한 모드 make -s

실무 Makefile 운영 플레이북

문법 이해만으로는 대규모 빌드를 안정적으로 운영하기 어렵습니다. 실제 프로젝트에서는 재현성, 증분 정확성, 디버깅 가능성을 동시에 관리해야 합니다.

안정성 규칙

재현성 점검 루틴

# 1) 깨끗한 상태에서 빌드
make clean
make -j$(nproc)

# 2) 무변경 재빌드 (아무것도 다시 빌드되면 의존성 선언 의심)
make -j$(nproc)

# 3) 드라이런으로 실행 명령 확인
make -n

# 4) 어떤 이유로 재빌드되는지 추적
make --debug=b target 2>&1 | less
증상 주요 원인 대응
매번 전체 재빌드 PHONY 오남용, 타임스탬프 오염 실제 파일 타겟/PHONY 분리, 생성 파일 경로 정리
병렬 빌드에서 간헐 실패 숨은 의존성 누락 명시 의존성 추가, 공유 출력 파일 제거
헤더 변경이 반영되지 않음 .d 의존성 미생성 -MMD -MP + -include $(DEPS) 적용

참고 자료 (References)