make
는 그 자체로 무척 강력하나 다음과 같은 상황에서 다양한 문제를 야기할 수 있다:
make
가 여러 디렉토리에서 의존 관계를 확인하고, 확장자 규칙을 적용하려면 별도의 작업이 필요하다.#if
와 같은 조건부 전처리기 지시자를 사용하는 경우에는 매우 복잡해질 수 있다..h
헤더 파일과 관련된 의존 관계. make
에 이와 같은 의존 관계를 명시적으로 언급해야 한다. 더미 타깃(더미 필요항목)을 사용하면 독자는 파일 간에 연관된 의존 항목에 관여하지 않으면서 작업을 할 수 있다. make
가 실제 파일의 생성 시간을 확인하는 반면, 더미 타깃과 필요항목에 대해서는 의존 관계에 대한 다소 임의적인 규칙을 사용한다.
make
는 타깃을 항상 다시 작성해야 한다.make
사용 모든 파일이 하나의 디렉토리에 위치할 때 make
를 사용하기 가장 쉽다. 그러나 대형 프로젝트에서는 거의 불가능한 일이다. 대개는 작성 과정에서 많은 프로젝트 멤버와 공유하는 디렉토리의 여러 파일과, 독자 자신의 개인적인 파일을 결합한 것이다.
대형 프로젝트에서 기술파일을 작성하는 가장 간단한 방법은 각 하위 디렉토리에 별도의 기술파일을 두고 마스터 기술파일에서 이들 make
를 재귀적으로 실행 하여 연결하는 것이다.
이런 방법은 조금 귀찮기는 하지만, 여러 디렉토리를 포괄하는 하나의 거대한 기술파일을 관리하는 것보다는 훨씬 쉽다.
make
를 재귀적으로 사용할 때의 TIP
일반적으로 make
를 재귀적으로 사용하면, 작성 중에 동적으로 정보를 결정하거나 작성의 다른 부분으로 정보를 전달하기 원할 때의 문제를 해결할 수 있다:
all:
make conv sys "FTYPE=$$(file a.out | grep -Po "(?<=: )[^,]*")"
sys:
@echo Building $@ with file type ${FTYPE}
...
conv:
@echo Building $@ with file type ${FTYPE}
MAKE
의 사용 위 예시에서는 기술파일이 make
명령어를 직접 참조했으나 이보단 내부 매크로를 사용하는 것이 더 좋다:
fig_obj:
cd fig; ${MAKE} all
이 방법의 장점은 MAKE
가 독자의 원래 명령행에 입력한 옵션을 전달한다는 것이다 (-t
, -n
, 그리고 -q
)
make
를 매번 실행시킬 때마다 새로운 세트의 매크로 정의와 함께 시작된다. 다시 말하면 make
는 최상위 레벨에서 설정한 매크로 정의를 자동으로 전달하지 않는다 는 것이다. 따라서 기술 파일의 모든 make
명령어는 중요하게 사용될 수 있는 매크로 등을 지정해야 한다:
all:
${MAKE} disp trac plot "CFLAGS=${CFLAGS}" \
"LDFLAGS=${LDFLAGS}" \
"INCLUDE=${INCLUDE}" \
"FRC=${FRC}"
제 2장 매크로
에서 설명한 것처럼 이들 명령행의 정의는 셸 환경 변수와 기술 파일에 포함된 매크로 정의보다 우선한다.
기존 정의를 변경하지 않고 옵션을 추가하길 원한다면 아래와 같이 작업할 수 있다:
${MAKE} disp trac plot "CFLAGS=${CFLAGS} -DBSD"
make
는 시스템의 다른 경로에 위치한 파일을 지정하는 기법을 제공한다.
책에서는 F 변경자
와 D 변경자
에 대해 소개하나 이들은 semi-obsoleted 되었기 때문에 설명을 생력한다. (The GNU make Reference Manual Version 4.2; 122pg.)
VPATH
매크로) make
의 일부 버전 (지금은 사실상 다 지원)에서는 VPATH
라는 이름의 매크로로 필요한 파일을 찾는데 사용하는 디렉토리의 리스트를 지정할 수 있다.
이와 같은 디렉토리 리스트를 탐색 경로(viewpath)라고 부르며, 이는 셸에서의 PATH
변수처럼 동작한다.
VPATH=/usr/src
OBJS=main.o allocate.o delete.o
structops: ${OBJS}
${CC} -o $@ ${OBJS}
main.o: main.c
allocate.o: allocate.c
delete.o: delete.c
다음의 makefile
은 필요항목을 찾을 때 두 곳을 모두 찾는다. 먼저 현재 디렉토리를 탐색하고, 다음으로 /usr/src
를 찾게 된다. 일부 make
버전에서 VPATH
를 독자가 명시적으로 표시한 의존항목 행의 필요항목을 검색할 때에만 사용한다는 것은 단점이 될 수도 있다. (GNU Make 에서는 탐색 경로를 의존 항목과 타깃 둘 다에서 쓸 수 있다. 25pg.)
또한 VPATH
는 여러 개의 디렉토리를 콜론으로 구분하여 지정할 수 있다.
VPATH
는 유용한 도구인 한편, make
의 버전마다 VPATH
를 사용하는 방식이 다른 경우가 많아 혼란이 생기기 쉽다. 발생할 수 있는 혼란을 피하기 위해 VPATH
를 연속되는 의존항목의 첫 번째 단계(소스 파일을 탐색하는 것)에서만 사용하기를 추천한다.
책에서는 아주 복잡하고 기괴한 방법으로 (그 당시에는 영리한 발상이었으리라) make
를 속여서 강제로 빌드를 수행하는 데 지금에는 사용이 어려운 (가능하나 복잡한) 방식이다.
이를 해결하기 위한 목적으로 설계된 PHONY 타깃
(29pg.) 을 사용하면 만사형통이므로 이를 사용하면 된다.
그 다음으로 이전에 버전 관리 기법을 소개하나 너무 구시대적 유물이라서 생략하려 한다. 지금은
git
이라는 훌륭한 프로그램이 있으므로 버전 관리에 그리 애를 쓰지 않아도 되며, 빌드 방식에 따른 산출 파일의 구분은 디렉토리만 구분하면 되는 문제이므로 축소한다.
make
는 숨겨진 의존 관계를 살펴보기 위해 파일 내부를 들여다 볼 수 없다. 따라서 어떠한 #include
지시자가 파일 내에 있는지 알지 못하며, 헤더 파일이 변경된 것을 확인하여 오브젝트 파일을 다시 작성할 수는 없다. 이런 문제를 해결하기 위해 대부분의 기술파일은 의존 관계를 직접 작성한다:
OBJS = parse.o search_file.o global.o find_token.o
{OBJS}: parse_defs.h
global.o: extra_defs.h
일부 C 컴파일러(BSD 와 썬 마이크로 시스템 사 버전 등; 지금은 gcc
에서 기본으로 제공)는 -M
옵션을 포함하고 있다. 이 옵션은 기술 파일에서 적합한 의존 리스트를 만들어 낸다. 이는 의존 관계를 만드는 데 상당히 안정적이며, 직접 작성한 것과 유사하다.
(이 특징은 모든 버전에서 이용할 수 있는 것은 아니다; 2023년의 make
에는 include
가 대부분 존재한다. 13pg. 이 기능이 make
에 없다면 필자의 귀싸대기를 갈겨도 좋다.)
대형 프로젝트에서 기술 파일이 여러 군데서 사용되면 (일반적으로 소스 트리에서 하나의 디렉토리마다 하나의 기술 파일이 사용), 프로젝트 팀에서는 매크로 정의와 확장자 규칙을 한 곳에 모으는 것에 대해 생각해 보아야 한다. 이는 일반 전처리기 정의를 여러 헤더 파일에 모으는 것과 유사하다. 이와 같은 통합을 지원하기 위해 make
는 include
문을 제공한다. 이 구문의 포맷은 아래와 같다:
include <file>
include
파일에 의존항목 행을 둔다면, 매크로를 참조하는 의존 항목 행 이전에 해당 매크로가 정의되어 있어야만 한다. 다시 말하면 아래와 같은 타깃을 include
파일에서 포함 한다면
install: ${EXECS}
사용자가 기술 파일에서 개별적으로 EXECS
을 정의하도록 만들어서는 안된다. include
문 다음에 위치한 EXECS
의 정의는 인식되지 않는다. 정의는 다음 중 하나의 위치에 있어야만 한다:
include
파일 내에 의존 항목 행 이전include
문 이전make
명령 행[Book] The GNU Make Reference Manual Version 4.2 (Richard M. Stallman, Roland McGrath, Paul D. Smith), A GNU Manual
[Book] Make: 유닉스, 리눅스 필수 유틸리티 (앤드류 오람, 스티브 탈보트 저; 이석주 역)