Search
Duplicate
🏭

Makefile에 헤더 파일의 의존성 정보를 자동으로 반영시키기

간단소개
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
Makefile
C
C++
태그
makefile
Scrap
8 more properties

개요

어떤 소스 파일이 어떤 헤더 파일을 포함하는지 컴파일러가 파악하여 의존성 정보를 작성하고, 이를 Makefile에 삽입하여 해당 헤더 파일이 변경될 시 이에 의존하는 소스 파일만 다시 컴파일하게 하는 과정을 자동화한다.

세줄요약

CFLAGS = -MMD -MP DEP = $(patsubst %.c,%.d,$(SRC)) -include $(DEP)
Makefile
Makefile에 적절히 추가한다.

문제 인식

의존성을 활용한 효율적 컴파일

Make를 사용해서 컴파일 과정을 자동화하는 데 있어서 가장 큰 장점은 파일의 의존성(dependency)를 명시하여 어떤 소스 코드가 변경되었을 시 해당 부분만 다시 컴파일하여 바이너리에 다시 링크하여 넣을 수 있다는 점이다. 이는 컴파일 시간을 크게 줄여주고 성질 급한 한국인의 수명 연장에 큰 기여를 한다.
이러한 의존성은 보통 다음과 같이 표현한다.
%.o: %.c
Makefile
이렇게 적어놓으면 Make는 foo.o 를 만들어야 할 때 foo.c 부터 찾을 것이고, 파일이 없다면 멈출 것이며, 파일이 존재하며 변경되지 않았을 경우 굳이 다시 foo.o 를 만들 지 않는다.

Make가 모르는 의존성

하지만 한 오브젝트 파일을 그 원본 소스에만 의존하지 않는다. 예를 들어 foo.c 에서 사용할 #define구문을 bar.h 에서 정의한다고 하자.
// bar.h #define SCREEN_W 1920 #define SCREEN_H 1080 //foo.c #include "bar.h" int main() { // ... mlx_new_window(mlx, SCREEN_W, SCREEN_H, "foo"); // ... return 0; }
C
이 상태에서 make 를 하면 foo.o 가 생성되는데, 이 파일 안에서 mlx_new_window에는 1920, 1080이 인자로 들어간다.
이때 이후 bar.h에서 #define된 상수를 변경하여도 foo.o를 다시 컴파일하지 않는다. 이 둘은 실제로는 의존하는 관계이지만 Make가 이를 알 방법이 없기 때문이다. foo.c가 변경되었을 때만 다시 컴파일이 이루어진다.
위 사례에서는 스크린 사이즈에 관련된 비기능적 문제이지만 경우에 따라서는 이유를 유추하기 어려운 오류의 원인이 되기도 한다.
// spam.h struct spam { int egg; } // foo.c int print_foo(){ struct spam s = {1}; printf("%d eggs\n", s.egg); return 0; } // bar.c int print_bar(){ struct spam s = {2}; printf("%d eggs\n", s.egg); return 0; }
C
이러한 3개의 파일이 있다 하자. 이때 spam.h에서 egg의 타입을 double로 변경하고 bar.c 또한 그에 맞춰 수정한다.
// spam.h struct spam { double egg; } // foo.c int print_foo(){ struct spam s = {1}; printf("%d eggs\n", s.egg); return 0; } // bar.c int print_bar(){ struct spam s = {2.0}; printf("%f eggs\n", s.egg); return 0; }
C
이렇게 수정한 후 make를 하면 foo.c는 수정되지 않았기 때문에 bar.c만 다시 컴파일 되고, 목적 바이너리가 있다면 bar.o만 수정되어 다시 링크될 것이다. 이때 foo.o는 여전히 egg의 타입은 int로 생각하고 있기 때문에 문제가 생길 것이다. 하지만 정상적으로 컴파일이 되기 때문에 이러한 오류에 익숙하지 않은 사람은 시간을 낭비하게 될 수도 있다.

임시 방편

이러한 문제는 헤더 파일을 변경할 때마다 make re를 실행하면 해결할 수 있다. 하지만 프로젝트가 커질 수록 컴파일 시간이 늘어나게 되고 코딩/디버깅의 리듬을 해치게 된다.
다른 방법으로는 의존성 정보를 Makefile에 직접 명시해 줄 수 있다.
foo.o: foo.c spam.h bar.o: bar.c spam.h %.o: %.c
Makefile
이렇게 하면 spam.h가 변경되면 foo.o, bar.o는 다시 만들어지지만 다른 *.c 파일들은 다시 컴파일하지 않는다. 하지만 개발 과정에서 #include를 작성하거나 지울때마다 그에 맞게 수동으로 Makefile을 수정할 수는 없는 노릇이다.

문제 해결

컴파일러 설정

어떤 소스가 어떤 헤더를 포함하는지는 프로그래머보다 컴파일러가 더 잘 알고 있다. gcc에는 이미 의존성 정보를 생성하는 옵션이 있다.
gcc -MMD -MP
Bash
-MMD 옵션을 켜면 컴파일러가 %.d파일에 Makefile 서식으로 소스 파일이 참조하는 시스템 헤더가 아닌 헤더 파일의 목록을 작성한다. -MP 옵션을 켜면 의존하는 헤더가 없어도 일단 빈 의존성 파일을 생성한다.
위 옵션과 함께 위 예제를 컴파일하면 다음 파일들이 추가로 생성된다.
# foo.d foo.o: foo.c spam.h spam.h: # bar.d bar.o: bar.c spam.h spam.h:
Makefile

Makefile 매크로 설정

이 내용이 Makefile에 자동으로 반영되면 문제 인식 단원에서 언급한 의존성 정보를 Make에게 알려줄 수 있게 된다.
먼저 소스 파일과 이름은 같고 확장자는 다른 파일 목록을 선언한다.
SRCNAME = foo bar SRC = $(addsuffix .c, $(SRCNAME)) OBJ = $(addsuffix .o, $(SRCNAME)) DEP = $(addsuffix .d, $(SRCNAME))
Makefile
이때 gcc -MMD -MP$(SRC)를 컴파일하면 $(OBJ)$(DEP)파일이 각각 생성된다.
Makefile 중간에 다음 줄을 삽입한다.
-include $(DEP)
Makefile
이렇게 하면 모든 %.d 파일의 내용이 Makefile 안에 삽입되게 된다. 이때 include 앞에 -를 붙이지 않으면 $(DEP) 파일이 존재하지 않을 시 오류가 발생한다. 소스 파일밖에 없는 상태에서 최초로 make를 구동하면 당연히 %.d파일이 없기 때문에 -를 붙여 오류를 무시하도록 한다.
위 과정을 거치면 Makefile은 다음과 비슷한 모습이 될 것이다.
SRCNAME = foo bar SRC = $(addsuffix .c, $(SRCNAME)) OBJ = $(addsuffix .o, $(SRCNAME)) DEP = $(addsuffix .d, $(SRCNAME)) $(NAME): $(OBJ) gcc $(OBJ) -o $@ -include $(DEP) %.o: %.c gcc -MMD -MP -c $< -o $@
Makefile
이제 최초로 make를 실행할 시 모든 %.c 파일이 %.o 파일로 컴파일 되면서 각각의 의존성 정보가 %.d 파일에 저장된다. 헤더 파일을 수정 후 다시 make할 시 -include $(DEP)에서 해당 의존성이 발동하여 해당하는 소스 파일만 다시 컴파일된 후 바이너리에 링크된다.