최고의 예제와 함께 Makefile 배우기
(makefiletutorial.com)- 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 파일 컴파일 기본 예제
-
blah.c
파일 생성(int main() { return 0; }
내용) - 다음 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: $
Hacker News 의견
-
1985년에 Boston University Graphics 랩에서 한 사람이 Makefile을 이용해 애니메이션용 3D 렌더러를 만들던 모습을 직접 본 경험이 있음. 그 사람은 Lisp 프로그래머로, 초기 프로시저 생성 및 3D 액터 시스템을 진행 중이었고, 10줄 정도로 구성된 정말 우아한 Makefile을 만듦. 단순한 파일 날짜 의존성으로 수백 개의 애니메이션을 자동 생성하는 구조였음. 각 프레임의 3D 형태를 Lisp로 만들고, Make가 프레임을 생성하는 방식이었음. 1985년 당시 3D와 애니메이션을 당연하게 여긴 요즘과 달리 모두가 놀라워하는 상황이었고, 그가 이후 Iron Giant와 Coraline의 3D 렌더러를 담당했던 Brian Gardner라는 점 기억
-
혹시 이 사람 3d-consultant.com/bio.html에 나오는 사람인지 궁금증 표현
-
Coraline이라는 영화를 말한 거 맞는지 확인
-
-
Make를 쓸 때 잘 알려지지 않은 유용한 플래그 몇 가지 소개
-
--output-sync=recurse -j10
: 의미는 각 타겟 작업이 끝날 때까지 stdout/stderr를 모아서 출력하는 플래그로, 그렇지 않으면 로그가 섞여서 분석이 어려움 - 바쁜 시스템이나 다중 사용자 환경에서는
-j
대신--load-average
를 활용하여 병렬 처리 시 시스템 부하를 조절할 수 있음 (make -j10 --load-average=10
) - 빌드 타겟 스케줄을 무작위로 섞는
--shuffle
옵션은 CI 환경에서 Makefile 내 의존성 문제를 잡아내는 데 유익
-
make의 다양한 옵션을 공식적으로 정리해 텍스트나 문서 형태로 프로그램에 포함시키면 사용 접근성이 높아진다는 아이디어 언급
-
본인이 자주 쓰는 옵션은 전체 강제 빌드에 사용하는
-B
플래그 설명 -
‘make -j’로 인해 도스 머신에서 발생한 문제를 자주 봤기 때문에 그 현상을 버그로 인식
-
바쁜 시스템이나 다중 사용 환경에서 병렬화 문제는 OS 스케줄러가 처리해야 하는 일 아닌지 질문
-
유용한 플래그지만 이 옵션들은 포터블하지 않기 때문에, 본인만을 위한 비공개 프로젝트 외에는 쓰지 말 것을 권장
-
-
.PHONY를 사용하지 않는다는 이유로 튜토리얼에서 건너뛰는 건 약한 변명이란 생각. 툴을 제대로 쓰는 방법을 가르치는 게 맞다는 의견
- 팀에서는 Make를 태스크 러너로 사용하면서 모든 레시피에 .PHONY를 추가 및 유지한 것 때문에 논쟁을 겪음
- Clark Grubb의 Makefile 스타일 가이드(clarkgrubb.com/makefile-style-guide) 추천
- .PHONY 선언을 레시피별로 하는 것과 파일 상단에 한 번에 모으는 것 사이에서 다양한 스타일 경험 공유 및 linter로 강제했으면 좋겠다는 바람
- 읽어본 결과 괜찮은 문서지만 몇 가지 동의하지 않는 점 있음
- -o pipefail을 맹목적으로 적용하는 건 문제이고, 파이프에서 grep 등을 쓸 때 깨질 수 있으니 상황별로 적용할 것 추천
- 비파일 타겟에 .PHONY를 마크하는 게 엄밀하긴 하지만 거의 불필요하고 Makefile만 장황해져서 필요시만 적용이 낫다는 관점
- 여러 개의 아웃풋 파일을 만드는 레시피는 예전에는 더미 파일을 썼지만, 최근 GNU Make 4.3부터 그룹 타겟 공식 지원(여기서 확인)이 가능
-
Make는 대형 C 코드베이스의 빌드에 특화된 툴이라는 주장
- 누군가 프로젝트별 잡 러너(업무 실행기)로 즐겨쓰는데, Make는 잡 러너로서는 적합하지 않고 조건문 같은 것도 어렵게 만드는 구조임
- Terraform 같은 도구와 래핑하려다 실패한 경우도 본 경험
-
Make는 잡 러너라기보다 선형 쉘 스크립트를 선언형 의존성 형태로 변환하는 범용 쉘 도구라는 의견
-
C 코드베이스만의 빌드 툴로서 Make를 보는 관점은 더 이상 맞지 않다는 입장. 지난 20년간 더 견고하고 명확한 빌드 시스템이 개발된 현실을 언급. 업데이트 필요성 제기
-
좋은 잡 러너에 대한 질문. (본인이 잡 러너 의미 혼동했다는 사과 추가)
-
Makefile이 복잡해지는 부분을 현대적으로 대체해주는 툴로 just 추천
-
just는 shell 스크립트 목록 대체에는 좋지만, ‘재실행이 필요한 룰만 실행’하는 Make의 본질적인 기능은 대체하지 못함
-
그 밖에 대안으로
- Task(Go) go-task/task
- Cake(C#) cake-build/cake
- Rake(Ruby) ruby/rake
- 완전히 다른 개념의 Makedown: HN 토론에서 논의
-
대안 도구들은 자신을 Make 대체라고 하지만 본인은 완전히 다르고 비교 자체가 어렵다고 생각. Make의 핵심은 산출물 생성과 이미 빌드한 것의 미재빌드에 있음. 반면 just는 단순 커맨드 실행기 역할
-
Make를 명령 실행기로 쓸 때의 장점은 거의 모든 곳에 설치되어 있는 표준 도구란 안정성. 대안들이 더 잘 만들어졌어도 별도 설치 부담이 있어 굳이 쓸 필요성을 못 느낌
-
Task는 내가 C로 하는 간단한 취미 프로젝트엔 잘 쓰고 있지만, 대형 프로젝트에도 적합한진 아직 판단 어려움(Task 공식 홈페이지)
-
-
최근 CMake가 Makefile이 C++20 모듈 지원에는 적합하지 않다고 보고 ninja를 기본으로 택했다는 점 흥미로움(CMake 가이드)
- 실제로는 타겟 의존성을 정적으로 정의하기가 불가능에 가까워서
clang-scan-deps
같은 도구로 동적으로 분석하는 방식 채택(기술 슬라이드)
-
실제로 이 제약은 CMake 쪽 결정이거나 Makefile generator에 지원자가 없는 문제라 생각. ninja도 C++ 모듈 직접 지원하지 못하고(관련 이슈), ninja는 오히려 Make보다 기능이 적고 모든 의존성을 정적으로 명시해야 한다는 문제 지적
-
모듈 도입 자체가 복잡하고 혼란스럽다는 의견
- 실제로는 타겟 의존성을 정적으로 정의하기가 불가능에 가까워서
-
tup 사용 경험이 있는지 질문. (공식 문서)
- tup은 파일 시스템 접근을 기반으로 자동으로 의존성을 파악해 어떤 컴파일러/툴에도 적용 가능한 빌드 시스템임
-
본인이 Task라는 Make 대안 툴의 창시자이자 메인테이너임을 소개. 8년 넘게 개발 중이고 계속 발전함
- 새로운 경험을 원한다면 한 번 써 보길 권유, 궁금한 건 언제든 질문 환영
- Task 공식 홈페이지, GitHub 저장소 링크
-
just 역시 또 다른 Make 대안으로 추천(just GitHub)
-
재미있는 우연으로, 본인은 Task를 자주 쓰고 오늘 아침에도 이슈 올림
-
이 튜토리얼에는 위험하고 미묘한 문제가 있음
- MAKEFLAGS에서 옵션 파싱 시, 긴 옵션이나 빈 짧은 옵션 다루려면 다음처럼 해야 함
ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))
- OS X 기본 제공 구버전 make 호환이 필요하다면 상당수 기능이 미비하거나 미묘하게 다름
- 이외의 문제는 대부분 오타나 최선의 스타일 위반이므로 생략
- 참고로 load는 guile보다 포터블하며, 크로스 컴파일 환경에서는 컴파일러 지정을 정확히 해야 함
- Paul’s Rules of Makefiles(여기)와 GNU make 매뉴얼(여기), 관련 매뉴얼을 꼭 읽어볼 것 추천
- 간단한 데모용 Makefile 프로젝트도 운영 중(데모 github)
- MAKEFLAGS에서 옵션 파싱 시, 긴 옵션이나 빈 짧은 옵션 다루려면 다음처럼 해야 함
-
각 GitHub 레포에 항상 Makefile을 포함하는 습관
- 매번 명령어를 까먹기 쉬워서 Makefile로 저장하면 손쉽게 복잡한 Step도 추가해둘 수 있고,
make
만 돌리면 따로 기억하지 않아도 프로젝트별 기대하는 동작 바로 실행 가능
- 매번 명령어를 까먹기 쉬워서 Makefile로 저장하면 손쉽게 복잡한 Step도 추가해둘 수 있고,