2P by GN⁺ 9시간전 | ★ favorite | 댓글 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: $
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의 본질적인 기능은 대체하지 못함

    • 그 밖에 대안으로

    • 대안 도구들은 자신을 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년 넘게 개발 중이고 계속 발전함

    • 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)
  • 각 GitHub 레포에 항상 Makefile을 포함하는 습관

    • 매번 명령어를 까먹기 쉬워서 Makefile로 저장하면 손쉽게 복잡한 Step도 추가해둘 수 있고, make만 돌리면 따로 기억하지 않아도 프로젝트별 기대하는 동작 바로 실행 가능