Make 규칙 분석 — 타겟·의존성·레시피의 평가
#규칙의 해부
Ch 1에서 본 것처럼 Makefile은 규칙(rule)의 묶음입니다. 규칙 하나의 모양은 단순합니다.
타겟: 의존성1 의존성2 레시피1 레시피2겉보기에는 세 줄짜리 문법이지만, Make는 이 작은 구조 위에 다중 타겟, 분리 의존성, order-only, 레시피 접두사, 이중 콜론, .ONESHELL 같은 손잡이를 마련해 두었습니다. 각각이 언제 필요한지, 안 쓰면 무엇이 어색해지는지를 함께 살펴봅니다.
#타겟(Target)
타겟은 대부분의 경우 만들려는 파일의 이름입니다. Make는 그 파일을 “최신 상태”로 유지하는 데 필요한 일을 합니다.
#단일 타겟
hello.o: hello.c hello.h gcc -c hello.c -o hello.ohello.o라는 파일이 hello.c와 hello.h보다 새것이 아닐 때 레시피가 돕니다. 가장 흔한 형태입니다.
#다중 타겟 — 한 번에 여러 개 묶기
같은 의존성과 같은 레시피 패턴을 공유하는 타겟이라면 한 줄에 묶어 적을 수 있습니다.
foo.o bar.o: common.h $(CC) -c $< -o $@이 한 줄은 내부적으로 다음 두 규칙으로 펼쳐집니다.
foo.o: common.h $(CC) -c $< -o $@
bar.o: common.h $(CC) -c $< -o $@각 타겟이 독립된 규칙으로 복제된다는 점이 중요합니다. 즉 make foo.o를 부르면 foo.o용 레시피만, make bar.o는 bar.o용 레시피만 실행됩니다. 두 타겟이 한 번의 명령으로 동시에 만들어지는 게 아닙니다. 정말로 “한 명령이 두 파일을 동시에 만든다”고 알려 주고 싶다면 4.3에 추가된 grouped target 문법(&:)을 써야 합니다(Ch 4에서 다룹니다).
$<(첫 번째 의존성)와 $@(타겟 이름)은 자동 변수입니다. 자세한 동작은 Ch 3에서 다룹니다. 지금은 손잡이로만 봐 두세요.
#기본 타겟
make만 입력하면 Make는 Makefile의 첫 규칙의 첫 타겟을 빌드합니다. 이것이 기본 타겟입니다. 사람들이 흔히 의심하는 “왜 위쪽 규칙이 더 중요해 보이지?”라는 직관이 사실은 정답입니다.
이 동작은 유연하면서도 위험합니다. 자칫 clean이 위에 가 있는 Makefile에서 그냥 make를 치면 빌드 산물이 통째로 날아갑니다. 그래서 관례는 단 하나입니다.
첫 타겟은 항상
all로 둔다.
.PHONY: all clean
all: hello goodbye
hello: hello.o gcc -o hello hello.o
goodbye: goodbye.o gcc -o goodbye goodbye.o
clean: rm -f hello goodbye *.o이 패턴이면 make만 쳐도 all이 평가되고, all이 hello와 goodbye에 의존하므로 둘 다 빌드됩니다. all은 파일을 만들지 않는 동작 이름이라 Ch 1에서 본 .PHONY 보호를 받습니다.
#의존성(Prerequisites)
의존성은 타겟이 완성되기 전에 먼저 준비되어 있어야 하는 파일입니다. Make는 의존성 파일의 mtime을 타겟과 비교해서 재빌드 여부를 정합니다. 이 mtime 비교가 Make의 거의 모든 결정을 떠받칩니다.
#의존성 체인 — 한 번의 수정이 어떻게 전파되는가
hello: main.o hello.o gcc -o hello main.o hello.o
main.o: main.c hello.h gcc -c main.c
hello.o: hello.c hello.h gcc -c hello.c이 Makefile이 머릿속에서 의존성 그래프로 변환되는 모습은 다음과 같습니다.
main.c+hello.h→main.ohello.c+hello.h→hello.omain.o+hello.o→hello
이제 hello.h만 손대 봅시다.
hello.h의 mtime이main.o보다 새것 →main.o재컴파일hello.h의 mtime이hello.o보다 새것 →hello.o재컴파일- 새
main.o,hello.o의 mtime이hello보다 새것 →hello재링크
한 헤더의 수정이 그래프를 거꾸로 거슬러 올라가며 세 단계의 작업을 만들어 냅니다. 이 전파가 자동으로 일어난다는 점이 Make를 손으로 만든 빌드 스크립트와 구분 짓는 핵심입니다.
#의존성을 여러 줄로 쪼개기
같은 타겟의 의존성을 여러 줄에 나눠 적을 수 있습니다. 의존성은 합쳐지고, 레시피는 한 군데에만 있으면 됩니다.
main.o: main.cmain.o: hello.hmain.o: config.hmain.o: gcc -c main.c -o main.o위는 다음과 정확히 같은 의미입니다.
main.o: main.c hello.h config.h gcc -c main.c -o main.o언뜻 보면 무의미한 기법이지만, 자동 생성된 의존성 파일을 include 할 때 진가가 드러납니다. 컴파일러가 gcc -MMD로 만들어 둔 .d 파일을 그대로 include하면, Make가 이 형태로 의존성을 흡수합니다. 헤더 변경이 정확히 반영되는 빌드는 이 패턴 위에서 돌아갑니다(자세한 자동 의존성 생성은 Ch 6·Ch 7에서 다룹니다).
#Order-only 의존성 — “있기만 하면 OK”
| 기호 뒤에 적는 의존성은 order-only입니다. Make는 이 파일이 존재하는지만 확인하고, mtime은 무시합니다.
build/hello.o: hello.c | build gcc -c hello.c -o build/hello.o
build: mkdir -p build왜 일반 의존성이 아니라 order-only가 필요할까요? 답은 디렉터리의 mtime이 바뀌는 시점에 있습니다.
리눅스에서 디렉터리의 mtime은 그 안에 파일이 추가/삭제될 때마다 갱신됩니다. build/hello.o를 만드는 순간 build/의 mtime은 hello.o보다 더 새것이 됩니다. 다음 빌드에서 Make는 의존성 build가 build/hello.o보다 새것이라고 판단하고, 멀쩡한 hello.o를 또 컴파일합니다. 그 결과로 또 mtime이 갱신되고… 영원히 같은 일을 반복합니다.
Order-only는 이 함정을 피하는 정답입니다. “디렉터리가 없으면 만들어라. 있으면 신경 쓰지 마라”라는 의미를 정확히 표현하는 방법이기 때문입니다. 실무에서 mkdir -p build 패턴이 등장할 때 거의 무조건 | 뒤에 두는 이유가 이것입니다.
또 다른 흔한 케이스는 생성 도구 의존성입니다. 코드 생성기(예: protoc)는 한 번만 만들면 충분하고, 이후 mtime 변경으로 전체 재빌드를 유발하면 곤란합니다. 이때도 order-only가 답입니다.
output.h: schema.proto | $(PROTOC) $(PROTOC) --cpp_out=. $<
$(PROTOC): ./build_protoc.sh#레시피(Recipe)
레시피는 타겟을 만드는 셸 명령입니다. 반드시 탭 한 글자로 시작하고, 한 규칙 안에서 여러 줄을 가질 수 있습니다.
#셸이 누구인지부터
Make는 레시피를 어떤 셸로 실행할까요? POSIX 표준은 /bin/sh를 기본으로 정합니다. GNU Make도 별다른 설정이 없으면 /bin/sh를 씁니다. 윈도우 환경(MSYS2 등)에서는 sh.exe를 찾습니다. 즉 어떤 시스템에서도 같은 명령이 돌도록 만들고 싶다면 셸 호환 문법(POSIX sh)에 머무는 게 안전합니다.
bash 전용 기능을 쓰고 싶을 때는 SHELL 변수를 명시적으로 바꿉니다(아래 “셸 변경” 절).
#각 줄은 별도의 셸 — 가장 흔한 오해
Ch 1에서 잠깐 언급한 사실인데, 다시 강조할 만합니다.
레시피의 각 줄은 각자 새 셸 프로세스에서 실행됩니다.
이 사실을 잊으면 다음과 같은 함정에 빠집니다.
wrong: cd subdir pwd # subdir가 아니라 원래 디렉터리! ls # 마찬가지첫 줄의 cd는 그 줄을 실행한 셸에만 영향을 미치고, 그 셸은 줄이 끝나는 순간 종료됩니다. 다음 줄은 새 셸에서 시작하므로 작업 디렉터리는 Makefile이 호출된 원래 자리로 되돌아갑니다.
해결은 셋 중 하나입니다.
방법 1 — 한 줄로 잇기 (가장 흔함)
correct: cd subdir && pwd && ls방법 2 — 백슬래시로 줄 연결
correct: cd subdir && \ pwd && \ ls\로 끝낸 줄은 다음 줄과 합쳐져 한 셸 명령이 됩니다. 가독성을 위해 자주 씁니다.
방법 3 — .ONESHELL (GNU Make 3.82+)
.ONESHELL:
correct: cd subdir pwd ls.ONESHELL을 켜면 한 레시피의 모든 줄이 한 셸에서 실행됩니다. 직관에 가깝지만 두 가지 부작용이 있습니다.
- 에러 처리: 기본 동작은 각 줄별 종료 코드 확인입니다.
.ONESHELL은 그 단위가 레시피 전체로 바뀌어,set -e같은 셸 옵션을 직접 켜야 정확한 실패 감지가 됩니다. - 접두사:
@같은 줄별 접두사는 첫 줄에만 적용됩니다.
큰 프로젝트에서는 일관된 셸 동작이 더 중요해서, .ONESHELL 대신 && 연결을 선호합니다.
#레시피 접두사 — @, -, +
레시피 줄 맨 앞에 특수 문자를 붙여 동작을 조절할 수 있습니다.
| 접두사 | 의미 | 자주 쓰는 자리 |
|---|---|---|
@ | 명령어를 화면에 출력하지 않음 | @echo "..." 같은 사용자 친화 메시지 |
- | 비-0 종료 코드를 무시하고 다음 줄로 | -rm -f *.o (파일 없을 때도 OK) |
+ | dry-run·query 모드에서도 실제 실행 | 재귀 Make 호출 +$(MAKE) -C sub |
세 접두사 모두 같은 줄에 함께 쓸 수 있습니다.
clean: @-rm -f *.o # 화면에 안 띄우고, 실패해도 무시#@ — 명령은 살리되 출력은 죽이기
기본 동작에서 Make는 실행하기 직전에 그 명령 줄을 그대로 화면에 출력합니다. 일종의 “지금 이걸 합니다” 알림입니다.
hello: echo "Building hello..." gcc -o hello main.o$ make helloecho "Building hello..."Building hello...gcc -o hello main.oecho "Building hello..."가 두 번 보이는 이유는, 첫 번째가 Make의 “지금 이걸 합니다” 알림이고 두 번째가 echo의 실제 출력이기 때문입니다. 사용자 친화 메시지에는 보통 첫 번째 출력이 거슬려서, @로 죽입니다.
hello: @echo "Building hello..." gcc -o hello main.o$ make helloBuilding hello...gcc -o hello main.o전체 출력을 한 번에 끄고 싶으면 make -s(silent) 또는 MAKEFLAGS += --silent를 씁니다.
#- — 실패 무시
clean: -rm -f *.o -rm -f hello @echo "Cleaned."rm이 없는 파일을 지우려 할 때 비-0 종료 코드를 돌려주는데, 그 경우에도 Make가 중단되지 않게 합니다. clean 같은 정리 타겟에서 자주 봅니다. 다만 rm -f처럼 도구 자체에 이미 “조용히 실패” 옵션이 있을 때는 -를 굳이 붙일 필요가 없습니다.
#+ — 강제 실행
make -n(dry-run, 명령을 출력만 하고 실제로 실행 안 함)이나 make -q(query, “최신인지 확인만”) 모드에서도 실제로 실행하라는 표식입니다. 재귀 Make 호출에서 거의 항상 붙는데, 부모 Make의 dry-run이 자식 Make에도 전달되어야 (자식이 또 dry-run을 하도록) 일관된 동작이 됩니다.
subdir: +$(MAKE) -C subdir$(MAKE)는 현재 실행 중인 Make 자신을 가리키는 특수 변수입니다. 단순히 make라고 적으면 환경 PATH에 있는 다른 Make가 실행될 수 있고, dry-run·jobserver 같은 설정이 끊깁니다.
#셸 변경
Bash 전용 기능([[, 배열, <<<, =~ 등)이 필요하면 SHELL 변수를 명시적으로 바꿉니다.
SHELL := /bin/bash
test: @echo "Bash version: $$BASH_VERSION" @[[ -f file.txt ]] && echo "exists" || echo "missing"여기서 :=는 즉시 확장(simply-expanded) 대입입니다. 일반 =은 지연 확장(recursively-expanded)이라 미묘하게 동작이 다른데, 자세한 이야기는 Ch 3에서 봅니다.
💡 왜
$$BASH_VERSION이지?: Make는$를 자기 변수 시작으로 봅니다. 그래서 셸 변수를 표시하려면$를 한 번 더 써서$$로 이스케이프해야 합니다. Make는$$를 만나면$한 글자로 줄여 셸에 넘기고, 셸이 그$BASH_VERSION을 자기 변수로 해석합니다.
엄격 모드를 켜고 싶으면 .SHELLFLAGS를 함께 바꿉니다.
SHELL := /bin/bash.SHELLFLAGS := -eu -o pipefail -c-e는 첫 실패에서 중단, -u는 미정의 변수 사용 시 에러, -o pipefail은 파이프라인 중 하나라도 실패하면 전체를 실패로 봅니다. 운영 스크립트에서 흔히 쓰는 안전 트리오로, Makefile에 들고 오면 디버깅이 크게 쉬워집니다.
#이중 콜론 규칙(::)
같은 타겟에 여러 독립 레시피를 정의하고 싶을 때 씁니다. 보통 콜론(:)으로는 같은 타겟을 두 번 적으면 오류이지만, 이중 콜론은 허용됩니다.
clean:: rm -f *.o
clean:: rm -f *.exe
clean:: rm -f *.logmake clean을 부르면 세 레시피가 위에서 아래 순서로 모두 실행됩니다.
언제 유용한가? 가장 흔한 경우는 여러 Makefile을 include해서 합칠 때입니다. 각 서브 모듈의 Makefile이 자기 몫의 정리 동작을 clean::으로 더하면, 상위 Makefile은 따로 합칠 필요 없이 자연스럽게 모든 clean이 호출됩니다.
clean:: rm -f common.o
# graphics.mkclean:: rm -f *.png
# 최상위 Makefileinclude common.mkinclude graphics.mk# 이제 `make clean`은 common.o와 *.png를 모두 지움단점은 의도 추적이 어려워진다는 것입니다. 누가 clean::을 추가했는지 한눈에 안 보이고, 실행 순서가 include 순서에 묶입니다. 그래서 이중 콜론은 플러그인 형태 빌드에서나 가끔 등장하고, 평범한 Makefile에서는 거의 안 씁니다.
#특수 타겟 — .PHONY 그 다음
.PHONY(Ch 1에서 본 것)는 Make가 내부 동작을 사용자에게 노출하는 방법입니다. 같은 점(.)으로 시작하는 특수 타겟이 여럿 있고, 각각이 Make의 한 가지 동작을 켜고 끄거나 바꿉니다. 다음 셋이 실무에서 가장 자주 등장합니다.
#.SECONDARY — 중간 산물 보존
Make는 암시적 규칙 사슬로 만들어진 중간 파일을 빌드 후 자동 삭제합니다. 예컨대 .y → .c → .o로 가는 사슬에서 .c가 중간 파일이면, 빌드가 끝난 뒤 사라집니다.
%.c: %.y yacc -o $@ $<
%.o: %.c gcc -c $< -o $@
myparser.o: myparser.y# yacc가 myparser.c를 만들고, gcc가 컴파일하고, 그 뒤 myparser.c는 삭제됨대부분의 경우는 이 동작이 원하는 결과입니다. 하지만 중간 파일을 디버깅용으로 남겨 두고 싶을 때가 있습니다. 그때 .SECONDARY를 씁니다.
.SECONDARY: # 모든 중간 파일 보존
# 또는 특정 파일만.SECONDARY: myparser.c빈 .SECONDARY:는 모든 중간 파일을 보존하라는 뜻입니다. 큰 자동 생성 코드(yacc/lex 출력)를 다룰 때 자주 켜 둡니다.
#.PRECIOUS — 중단 시 삭제 방지
Make는 레시피 실행 중에 인터럽트되면 부분적으로 만들어진 타겟을 삭제합니다. 깨진 산물이 다음 빌드를 망치는 사고를 막기 위해서입니다.
$ make myapp# 컴파일러가 myapp.o를 만들기 시작했는데...# (Ctrl-C로 중단)# Make가 자동으로 부분 myapp.o를 삭제대부분 환영할 동작이지만, 생성에 매우 오래 걸리는 산물(예: 외부 다운로드 + 빌드)에서는 한 번 만든 걸 잃기 싫을 때가 있습니다. 그때 .PRECIOUS를 씁니다.
.PRECIOUS: downloaded-archive.tar.gz
downloaded-archive.tar.gz: curl -O https://example.com/big-file.tar.gz.PRECIOUS로 묶인 파일은 중단 시에도 보존됩니다. 다음 빌드에서 그대로 재사용되어 다시 받지 않아도 됩니다.
.SECONDARY와의 차이: .SECONDARY는 빌드 후 자동 삭제 방지, .PRECIOUS는 중단 시 부분 산물 삭제 방지. 둘 다 켜면 어떤 상황에서도 안 지워집니다.
#.DELETE_ON_ERROR — 반대 방향, 실패 시 강제 삭제
기본적으로 Make는 레시피가 실패해도 그동안 만들어진 부분 산물을 삭제하지 않습니다. 이 동작이 의외의 사고를 부릅니다.
build/output.tar.gz: input.txt gzip -c $< > $@ # 만약 gzip이 도중에 실패하면?gzip이 부분적으로 출력을 쓰고 실패하면, output.tar.gz가 불완전한 상태로 디스크에 남습니다. mtime은 최신이라 다음 빌드에서 Make는 이미 만들어진 것으로 판단합니다. 결과: 깨진 산물을 가지고 빌드가 진행됩니다.
.DELETE_ON_ERROR는 이 함정을 막습니다.
.DELETE_ON_ERROR:
build/output.tar.gz: input.txt gzip -c $< > $@이제 gzip이 실패하면 Make가 부분 output.tar.gz를 자동으로 삭제합니다. 다음 빌드가 새로 시도합니다.
💡 항상 켜 두면 좋은 옵션입니다. 안 켜면 언젠가 위 사고가 납니다. 큰 Makefile 첫 줄(또는
cmake_minimum_required처럼 표준 헤더)에.DELETE_ON_ERROR:한 줄을 더하는 게 권장 관행입니다.
#그 외 자주 보는 특수 타겟
| 특수 타겟 | 의미 |
|---|---|
.PHONY: a b c | a/b/c가 동작 이름임을 알림. 같은 이름 파일이 있어도 매번 실행. |
.SUFFIXES: | 옛 접미사 규칙을 비우거나 추가. .SUFFIXES: (비우기)는 암시적 규칙 끄는 표준 패턴. |
.ONESHELL: | 한 레시피 안의 모든 줄을 한 셸에서 실행 (3.82+). |
.NOTPARALLEL: | 이 Makefile은 병렬 빌드 금지. 단계별 의존성이 자동 추적 불가할 때. |
.EXPORT_ALL_VARIABLES: | 이 Makefile의 모든 변수를 환경 변수로 자식 프로세스에 노출. |
.IGNORE: target | 이 타겟의 모든 레시피 실패를 무시. (-rm ...과 같은 효과지만 타겟 단위). |
.DELETE_ON_ERROR:와 .SUFFIXES: 두 줄을 Makefile 헤더 관용으로 두면 많은 사고를 미리 막을 수 있습니다.
#흔한 실수
처음 Makefile 작성 시 자주 부딪히는 자리들입니다.
#1. 디렉터리를 일반 의존성으로 넣기
# 문제: build에 파일 추가될 때마다 hello.o 재컴파일build/hello.o: hello.c build gcc -c hello.c -o build/hello.obuild/ 디렉터리는 그 안에 파일이 들어올 때마다 mtime이 갱신됩니다. 결과적으로 디렉터리가 항상 hello.o보다 새것이 되어 무한 재빌드가 일어납니다.
해결: order-only 의존성
build/hello.o: hello.c | build gcc -c hello.c -o build/hello.o#2. cd 후 다음 줄에서 작업
deploy: cd /var/www cp -r dist/* . # /var/www가 아닌 원래 디렉터리에서 실행!해결: 한 줄로 잇기
deploy: cd /var/www && cp -r $(CURDIR)/dist/* .$(CURDIR)은 Make가 시작될 때 작업 디렉터리를 담아 두는 자동 변수입니다. cd 이후에도 원래 디렉터리를 가리키므로 절대 경로처럼 안전합니다.
#3. 셸 변수에 $$ 안 붙임
test: for f in *.c; do echo $f; done # 빈 문자열 출력$f는 Make 변수로 해석되어 정의된 적 없는 빈 값이 됩니다.
해결: $$로 이스케이프
test: for f in *.c; do echo $$f; done#4. tab 대신 공백 들여쓰기 (반복)
여전히 가장 흔합니다. 에디터 설정을 의심하세요.
Makefile:5: *** missing separator. Stop.이 메시지가 보이면 99%는 탭이 아닌 공백 들여쓰기입니다.
#5. 첫 타겟이 clean
clean: rm -f *.o
hello: main.o gcc -o hello main.omake만 치면 *첫 타겟인 clean*이 실행되어 빌드 산물이 날아갑니다. 관습대로 첫 타겟은 all로 두세요.
#작은 예시 — 모든 요소 적용
지금까지의 도구를 한 자리에 모은 Makefile입니다.
.PHONY: all clean
SHELL := /bin/bash.SHELLFLAGS := -eu -o pipefail -c
BUILD := build
all: $(BUILD)/hello
# 실행 파일 — 두 오브젝트에서 링크$(BUILD)/hello: $(BUILD)/main.o $(BUILD)/hello.o | $(BUILD) gcc -o $@ $^
# 오브젝트 파일들 — order-only로 build 디렉터리 보장$(BUILD)/main.o: main.c hello.h | $(BUILD) gcc -c $< -o $@
$(BUILD)/hello.o: hello.c hello.h | $(BUILD) gcc -c $< -o $@
# 디렉터리 만들기 — 한 번만$(BUILD): mkdir -p $@
clean:: @echo "Cleaning..." -rm -rf $(BUILD)이 Makefile은 다음을 만족합니다.
all이 첫 타겟이므로 안전합니다.- 빌드 산물이
build/안에 격리되어 소스 디렉터리가 깨끗합니다. build/디렉터리는 order-only로 잡혀 무한 재빌드를 막습니다.- Bash strict 모드(
-eu -o pipefail)로 작은 실수도 즉시 멈춥니다. clean::은 이중 콜론으로, 추후 다른.mk파일이 정리 동작을 더하기 쉽습니다.
다음 장에서 변수와 자동 변수($@, $<, $^)를 배우면 이 Makefile을 한 번 더 줄일 수 있게 됩니다.
#정리
- 타겟·의존성·레시피가 한 규칙의 세 부분. 의존성이 새것이면 레시피가 돈다.
- 다중 타겟
a b: dep은 두 개의 독립 규칙으로 펼쳐진다. 진짜 그룹은&:문법(Ch 4). - Order-only
|는 존재만 검사. 디렉터리·생성 도구 의존성에 거의 필수. - 레시피의 각 줄은 독립 셸.
cd가 이어지지 않는 원인. 해결은&&·\·.ONESHELL. - 레시피 접두사:
@(출력 죽임),-(실패 무시),+(dry-run에도 실행). SHELL·.SHELLFLAGS로 Bash strict 모드를 켜 둘 만하다.- 이중 콜론
::은 같은 타겟 여러 레시피. 플러그인식 Makefile에서 가끔 쓴다.
#다음 장 예고
Ch 3: 변수에서는 Make의 변수를 다룹니다. 사용자 정의, 자동 변수($@, $<, $^, $?), 두 가지 확장 방식(= vs :=), 환경 변수와의 관계까지 — Make에서 “왜 같은 코드가 미묘하게 다르게 동작하지?”의 9할이 이 장에서 풀립니다.
#참고 자료
- GNU Make Manual — Writing Rules
- GNU Make Manual — Recipes
- GNU Make Manual — Special Targets (
.PHONY,.ONESHELL등)
관련 글
실전 Makefile 예제 — C/C++ 프로젝트용 기본 골격
기본 C/C++부터 라이브러리, 크로스 컴파일, 테스트 통합까지 — 실제 프로젝트에 그대로 쓰는 Makefile 패턴.
Make 조건문과 include — ifeq·ifdef·include·-include
파싱 시점 조건 분기, Makefile 분할, 그리고 -MMD -MP로 헤더 의존성을 자동 추적하는 표준 패턴.
Make 함수 분석 — wildcard·patsubst·foreach·shell
내장 함수로 텍스트·파일·조건을 다루기 — wildcard / patsubst / filter / shell / foreach / call / eval.