개요

지식의 출처

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

세줄요약

CFLAGS = -MMD -MP

DEP = $(patsubst %.c,%.d,$(SRC))

-include $(DEP)

Makefile에 적절히 추가한다.

문제 인식

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

Make를 사용해서 컴파일 과정을 자동화하는 데 있어서 가장 큰 장점은 파일의 의존성(dependency)를 명시하여 어떤 소스 코드가 변경되었을 시 해당 부분만 다시 컴파일하여 바이너리에 다시 링크하여 넣을 수 있다는 점이다. 이는 컴파일 시간을 크게 줄여주고 성질 급한 한국인의 수명 연장에 큰 기여를 한다.

이러한 의존성은 보통 다음과 같이 표현한다.

%.o: %.c

이렇게 적어놓으면 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;
}

이 상태에서 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 또한 그에 맞춰 수정한다.