어떤 소스 파일이 어떤 헤더 파일을 포함하는지 컴파일러가 파악하여 의존성 정보를 작성하고, 이를 Makefile에 삽입하여 해당 헤더 파일이 변경될 시 이에 의존하는 소스 파일만 다시 컴파일하게 하는 과정을 자동화한다.
CFLAGS = -MMD -MP
DEP = $(patsubst %.c,%.d,$(SRC))
-include $(DEP)
를 Makefile
에 적절히 추가한다.
Make를 사용해서 컴파일 과정을 자동화하는 데 있어서 가장 큰 장점은 파일의 의존성(dependency)를 명시하여 어떤 소스 코드가 변경되었을 시 해당 부분만 다시 컴파일하여 바이너리에 다시 링크하여 넣을 수 있다는 점이다. 이는 컴파일 시간을 크게 줄여주고 성질 급한 한국인의 수명 연장에 큰 기여를 한다.
이러한 의존성은 보통 다음과 같이 표현한다.
%.o: %.c
이렇게 적어놓으면 Make는 foo.o
를 만들어야 할 때 foo.c
부터 찾을 것이고, 파일이 없다면 멈출 것이며, 파일이 존재하며 변경되지 않았을 경우 굳이 다시 foo.o
를 만들 지 않는다.
하지만 한 오브젝트 파일을 그 원본 소스에만 의존하지 않는다. 예를 들어 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;
}
이 상태에서 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;
}
이러한 3개의 파일이 있다 하자. 이때 spam.h
에서 egg
의 타입을 double
로 변경하고 bar.c
또한 그에 맞춰 수정한다.