Make 기반 빌드 시스템 (8): make 재귀 호출에 의한 배치 빌드

make 재귀 호출에 의한 배치 빌드

지난 글에서, 하나의 라이브러리를 적절한 의미와 규모를 가지는 여러 개의 라이브러리들로 구성하되, 각 라이브러리의 개별 설정과 전체 라이브러리들의 일괄 변경을 쉽게 처리할 수 있도록 Makefile의 공용 include 파일들을 분리해 내고 이들을 쉽게 관리할 수 있는 빌드 디렉토리의 구성해 보았습니다.

이걸 가지고 하나로 빌드하던 라이브러리를 MyNetwork, MyThread, MyProtocol 이렇게 3개의 라이브러리들로 나누었고, 덕분에 나중에 다른 프로그램 작성할 때 MyNetwork와 MyThread의 기능을 쉽게 사용할 수 있게 되었습니다. 물론 이건 좋은 일입니다. 그런데, 좋긴 한데, 좋은 건 나중 일이고, 당장 MyClient 프로그램의 개발 담당자 입장에서 보면 오히려 좀 불편해졌습니다.

뭐냐하면 전에는 하나의 라이브러리로 되어 있으니까, thread든 network든 protocol이든 그냥 수정할 게 있으면 수정한 다음 그 자리에서 빌드하면 끝인데, 이제는 MyNetwork의 소스 파일 또는 여기에서 사용하는 헤더 파일을 수정하면, Network 디렉토리로 가서 빌드해 준 다음, 다시 Protocol 디렉토리로 와서 빌드해야 합니다. Thread도 마찬가지고요.

의존하는 라이브러리들이 많아질수록 더 많은 경로 이동과 make가 필요하게 됩니다. 이건 단순히 번거롭다 차원의 문제가 아니죠. 하나라도 깜빡 잊으면 링킹 에러 또는 런타임 에러가 되고, 그 결과 불필요한 시간 허비와 허탈한 한숨을 야기할테니까요.

서브 디렉토리 빌드

어떻하는 게 좋을까요… 사실 답은 간단합니다. 빌드하려는 라이브러리 또는 실행 바이너리가 다른 디렉토리에 있는 라이브러리에 의존하는 경우에는, 해당 라이브러리의 디렉토리로 이동해서 빌드를 먼저 수행하고 나서, 원래 디렉토리로 돌아와서 빌드를 이어할 수 있도록 해 주는거죠, 아, 물론 자동으로 말입니다.

일단 먼저 빌드되어야 하는 디렉토리를 나타내는 변수가 하나 필요한데, SUB_DIRS 라고 해 볼까요? SUB_DIRS 는 필요한 곳에서만 존재하면 될 테니, 공용 include 파일이 아닌 Makefile에서 정의합니다. MyNetwork, MyThread, MyProtocol 중에는 MyProtocol 만 필요하죠. MyProtocol 은 MyThread와 MyNetwork를 사용하기 때문에 해당 라이브러리들이 위치한 디렉토리들을 SUB_DIRS에 지정합니다.

다음 단계로 SUB_DIRS 에 지정된 디렉토리들로 이동해서 make를 실행시키는 subdirs 라는 규칙을 만들어 볼까요?

규칙의 타겟은 subdirs이고요, 선행조건은 없습니다. 좀 복잡해 보이는 레시피 부분은 bash 쉘 스크립트의 구문으로 이루어져 있습니다. (4)와 (7)은 $(SUB_DIRS) 의 각 항목에 대한 for 반복문인데, (4)는 for 반복문의 시작 부분으로, $(SUB_DIRS) 각 항목을 dir 이라는 변수를 지정하고 있고, (7)은 반복의 끝을 가리킵니다.

참고로 bash에서도 어떤 변수를 참조할 때에는 변수 이름 앞에 $를 붙이는데요, 그런데 $는 Makefile 내에서 변수를 가리킬 때에도 쓰이죠. 그래서 make에서 shell 변수를 사용할 때에는 shell 변수 이름 앞에 $를 하나 더 붙입니다. (웬지 Make 기반 빌드 시스템 (4): 라이브러리와 실행 파일의 분리 포스트에서 다뤘던 secondary expansion이 연상되지 않나요?) (4)에서 선언된 dir 변수가 $$를 두 개 붙인 건 (달라를 너무 좋아해서가 아니라) 이런 이유 때문입니다.

자, (5)가 바로 서브 디렉토리인 dir 디렉토리에서 make를 수행하는 핵심 부분인데요, make 뒤에 나오는 -C 옵션이 특정 디렉토리로 이동해서 make를 수행하는, 정확히 우리가 필요로 하는 기능입니다. (사실 이 부분은 cd $$dir; $(MAKE) all 이렇게 해도 되지만, make에서 쓰라고 만든 옵션을 굳이 안 쓸 필요는 없겠죠?)

(6)은 바로 위에서 실행한 make가 오류가 발생했을 때 예외 처리하는 부분인데요, bash에서 $? 변수는 이전에 실행된 명령어나 함수, 스크립트의 종료 상태를 나타내는 bash 쉘의 내장 변수입니다. 관습적으로 0은 성공을 나타내고 1에서 255까지의 숫자는 에러를 나타냅니다. 따라서 (6)은 (5)에서 실행한 (서브 디렉토리에서의) make가 성공하지 못했을 때는, 종료 상태를 1로 설정한 채 현재 make의 실행을 중단하라는 의미입니다.

subdirs 규칙은 이렇게 완성 되었으니, 이제 이 녀석이 라이브러리와 실행 바이너리보다 먼저 실행되게 하는 것만 남았네요. 어렵지 않습니다. all 규칙의 선행조건에, 라이브러리와 실행 바이너리를 나타내는 $(LIB_FULL_NAME)과 $(TARGET_NAMES) 앞에 들어가기만 하면 됩니다.

이렇게 수정하고 Prototype 디렉토리에서 make 를 수행해 볼까요?

(2)에서와 같이 SUB_DIRS 에 지정된 첫번째 디렉토리인 Thread 디렉토리로 이동해서 빌드하는 것을 볼 수 있습니다. 이후 빌드가 완료되고 나면 (12), (13)에서처럼 Thread 디렉토리를 나가고, 다시 Network 디렉토리로 이동합니다. 그리고 Network 디렉토리의 빌드가 완료되고 나면 (15)에서 Network 디렉토리를 나갑니다. 이후 원래의 Protocol 디렉토리의 빌드를 시작합니다.

make all 규칙을 가만히 들여다 보세요. 서브 디렉토리가 지정되어 있으면 all 은 선행조건인 subdirs 규칙을 통해 모든 서브 디렉토리에 대해 make all 을 실행하게 되는데, 만약 서브 디렉토리에 다시 서브 디렉토리가 지정되어 있으면, 그 서브-서브 디렉토리에 대해서 또다시 동일한 make all 을 실행합니다. 즉, 모든 서브 디렉토리들을 재귀적으로 빌드하게 되는 거죠.

여기서 make[1] 은 현재 make의 call stack 수를 의미합니다. Thread 디렉토리로 이동해서 빌드를 하려는데 여기에도 Makefile 의 SUB_DIRS 가 지정되어 있다면, 해당 서브-서브 디렉토리로 이동해서 빌드를 할 테고요, 그때는 make[2] 가 될 겁니다. 만의 하나 SUB_DIRS 구성이 잘못되어 순환 참조가 일어나면, 즉 Thread 의 SUB_DIRS 에 Protocol 디렉토리가 포함되어 있다면 메모리가 허용할 때까지 make를 시도하다가 결국엔 비정상 종료가 일어나게 됩니다.

만약 SUB_DIRS 에 지정된 디렉토리가 존재하지 않으면 어떻게 될까요? 예를 들어, $(PROJ_SRC_DIR)/Oops 디렉토리를 실제 디렉토리에는 추가하지 않은 채, SUB_DIRS에만 추가해 볼까요?

이렇게 하고 빌드를 수행하면, 현재의 make 실행 자체가 실패하면서 바로 중단됩니다.

그럼 Oops 디렉토리는 있는데 Makefile 이 없을 경우는 어떻게 될까요? 그때는 Oops 디렉토리에서 적절한 Makefile 이 없다며 make가 실패하고, 이후의 make 들이 중단됩니다. (디렉토리가 아예 없을 때와는 달리) 그 이전의 make들은 성공하고요.

참고로, 지금은 $(ROOT_SRC_DIR)/… 이렇게 절대 경로를 사용했습니다만, subdirs 규칙을 보면 어차피 SUB_DIRS 에 지정된 항목 하나하나에 대해서 make -C 를 수행하기 때문에, 유효하기만 하다면 현재 디렉토리에 대한 상대 경로를 사용해도 상관 없습니다. 예를 들어 SpecialSubDir 이나 ../Thread 와 같은 상대 경로 상의 디렉토리들도 SUB_DIRS 에 지정 가능하다는 것이죠.

dependall 과 cleanall

서브 디렉토리들에 대해 재귀적으로 빌드가 가능해졌으니까, 마찬가지로 clean 과 depend 도 재귀적으로 처리할 수 있도록 해야겠죠? 다음을 보세요.

dependall 과 cleanall 규칙들은 각각, 선행조건에 depend 와 clean 을 지정해서 현재 디렉토리에서 depend 및 clean 을 먼저 수행하고, 이후 서브 디렉토리들에 대해 재귀적으로 같은 작업을 수행합니다. 차이라면 all 은 subdirs 를 통해 재귀 호출은 한데 비해서, dependall 과 cleanall 은 자체적으로 재귀 호출을 수행한다는 것 뿐이네요.

그리고나서는 dependall 나 cleanall 를 실행할 때에는 depend_file 을 include 하지 않도록 해주면 끝입니다.

응용 규칙 추가

이제까지는 make all 을 실행해서 라이브러리와 실행 바이너리들을 한번에 빌드했습니다. 이건 Makefile에서 all 의 선행조건에 라이브러리(LIB_FULL_NAME)과 실행 파일들(TARGET_NAMES)를 지정했기 때문이죠. 그리고 선행조건 맨 앞에 subdirs 규칙을 넣어서 의존하는 서브 디렉토리의 라이브러리들이 먼저 빌드하도록 했는데요, 그 결과 항상 서브 디렉토리가 먼저 빌드되고 나서, 현재 디렉토리가 빌드됩니다. 네, 이게 우리가 원한 겁니다.

이렇게만 해도 충분합니다. 충분한 것 같은데, 빌드할 소스 파일들이 많아지고 의존하는 라이브러리들이 많아지면, 이런 저런 추가 요구 사항들이 생겨날 지도 모릅니다. 세상은 넓고 별난 개발자도 많은 법.

  • “서브 디렉토리의 소스 파일들보다는 현재 디렉토리에 있는 소스 파일들을 편집하는 빈도가 아무래도 더 높지 않겠어? 현재 디렉토리의 소스 파일에 컴파일 에러를 일으킬 문제가 있다면, 서브 디렉토리들 다 한번씩 갔다 와서 발견하는 것 보다는 빌드 시작하자 마자 바로 발견하는게 좋을 것 같은데…”
  • “난 내 라이브러리하고 실행 파일들만 빌드하고 싶은데, 서브 디렉토리들의 라이브러리들만 빌드하고 거기 있는 실행 파일들은 빌드 안 할 수는 없을까?”
  • 기타 등등…

먼저, 서브 디렉토리의 라이브러리들보다 현재 디렉토리의 라이브러리를 먼저 빌드하는 건 간단합니다. all 규칙의 선행조건에서 subdirs 와 $(LIB_FULL_NAME)의 위치만 바꿔 주면 됩니다. 쉽죠?

음… 그런데 $(LIB_FULL_NAME) 이나 $(TARGET_NAMES)는 Makefile 마다 제각각인게 좀 거슬립니다. 모든 곳에서 동일한 규칙 이름으로 빌드할 수 있다면, 사용하기가 좀더 편하지 않을까요? 추가하죠, 뭐. 어려운 것도 아닌데 말입니다.

lib 과 targets 규칙도 있는 마당에, 이제 서브 디렉토리의 실행 프로그램들만 빼고 빌드하는 liball 규칙의 작성은 이젠 누워서 떡먹기죠. 현재 디렉토리의 라이브러리가 먼저 빌드되도록, 선행조건에 lib 을 추가하고, $(SUB_DIRS) 의 각 디렉토리 항목마다 make 를 재귀적으로 실행하면 끝입니다.

liball 도 만들었는데 targetsall 도 같이 만들어 볼까요? 마찬가지로 현재 디렉토리의 소스 파일들이 먼저 빌드되도록 선행조건에 targets 를 둡니다.

흠, 규칙들을 꽤 많이 만들었네요. 한번 정리해 보죠.

  • all: 현재 디렉토리 및 모든 서브 디렉토리들의 라이브러리와 실행 프로그램들 빌드
  • subdirs: 모든 서브 디렉토리들의 라이브러리와 실행 프로그램들 빌드
  • clean / cleanall: 현재 디렉토리의 결과물만 제거 / 현재 디렉토리 및 모든 서브 디렉토리들의 결과물 제거
  • depend / dependall: 현재 디렉토리의 의존성 파일 생성 / 현재 디렉토리 및 모든 서브 디렉토리드들의 의존성 파일 생성
  • lib / liball: 현재 디렉토리의 라이브러리 빌드 / 현재 디렉토리 및 모든 서브 디렉토리드들의 라이브러리 빌드
  • targets / targetsall: 현재 디렉토리의 실행 프로그램들 빌드 / 현재 디렉토리 및 모든 서브 디렉토리드들의 실행 프로그램들 빌드

적어 놓고 보니, subdirs 만 빼고는 현재 디렉토리에서만 수행하는 규칙 이름에 all 이 붙으면 재귀적으로 수행하는 거네요.  굳이 subdirs 를 써야만 되는 경우는 별로 없을 것 같으니, 실제로는 lib, targets, clean, depend 이렇게 4개의 규칙과, 재귀적으로 빌드할 때는 all 을 붙인다 정도만 알면 크게 어렵지 않게 사용할 수 있을 겁니다.

라이브러리 의존성 규칙

Protocol 디렉토리의 MyClient 프로그램의 경우, lbMyProtocol.a, libMyThread.a, libMyNetwork.a 세 개의 라이브러리에 의존하고 있습니다. MyClinet.c 가 의존하고 있는 MyProtocol.h 파일이 수정되었다면, 다음에 make 를 실행했을 때 libMyProtocol.a 가 빌드되고, MyClient.c 도 컴파일되서 MyClient 가 링킹됩니다.

예를 들어 모든 헤더 파일에 include 되는 MyCommon.h 가 수정되면, 모든 .c 파일들이 컴파일될테고, 라이브러리들과 실행 프로그램들이 일제히 빌드되겠죠? 아, 이제까지 고생한 보람이 느껴집니다.

헌데 MyProtocol.c 파일을 수정하고 나서 make 를 실행해 보면 좀 이상한 결과가 나옵니다.

어라? MyProtocol.c 컴파일되고, libMyProtocol.a 는 다시 만들어지는데, MyClient 는 다시 빌드가 안 되네요? 이거 웬지 예전에 똑같은 경험을 한 것 같은 기시감이 느껴지지 않습니까? 네, Make 기반 빌드 시스템 (2): Makefile 기초 의 “의존성 규칙” 부분에서 거의 유사한 경험을 했죠.

그때는 .c 파일이 변경되면 컴파일이 되면서 다시 빌드되는데, .c 파일에서 include 한 헤더 파일들이 변경되었을 때는 컴파일이 되지 않았습니다. 그래서 의존성(depend) 규칙을 추가해서 해결했지요.

지금 실행 파일이 새로 링킹 되지 않는 것도 비슷한 이유입니다. 이제까지 작성한 Makefile에서, 실행 프로그램은 실행 프로그램에 포함된 구현 소스 파일과 거기서 include된 헤더 파일에만 의존할 뿐, 라이브러리와는 아무런 상관이 없습니다. 따라서 라이브러리의 구현 소스 파일이 변경되면 라이브러리만 빌드되지, 실행 프로그램들은 변경된 라이브러리를 다시 링킹하지 않습니다. 아, 치명적이네요.

하지만 문제의 원인이 같다면 해결 방법도 같을 테지요? 다시 말해, 실행 프로그램들이 라이브러리 파일에 의존적이라는 것을 알려주는 의존성 규칙을 추가해 주는 거죠. 다음처럼 말입니다.

위와 같은 규칙이 추가되면, 이젠 각 MyClient 같은 실행 파일이 세 라이브러리에 모두 의존적이란 걸 make 가 알게 되기 때문에, 세 라이브러리 중 하나라도 변경되면, 실행 파일들을 다시 링킹하게 됩니다.

좋습니다. 하지만 라이브러리 의존성 규칙을 추가한다는 건, libSomething.a 라는 라이브러리를 하나 사용하면, $(LIBS) 변수에 -lSomething 도 추가하고 라이브러리 의존성 규칙에 $(ROOT_LIB_DIR)/$(OBJS_DIR)/libSomething.a 도 추가해야 한다는 얘기가 됩니다. 이거 좀 번거롭다는 생각과 함께, $(LIBS) 로부터 $(TARGET_NAMES) 규칙을 자동으로 만들 수 있지 않을까… 싶지 않으신가요? (그러시다면 이제까지의 제 설명과 게으른 제 성격을 완전히 파악하신 겁니다.)

역시나 이제는 많이 익숙해진 make의 문자열 치환을 써서 $(LIBS) 의 항목들을 $(TARGET_NAMES) 의존성에 들어갈 것으로 쉽게 바꿀 수 있습니다. 자신 있으신 분들은 직접 한 번 해 보셔도 좋겠네요. 정답은 바로…

5

4

3

2

1

짜잔.

어떻게, 다들 맞추셨나요? 자 그럼 라이브러리 의존성 규칙은 다음과 같이 작성하면 되겠습니다.

그런데 여기서 하나 짚고 넘어가야 할 게 있습니다. 빌드하고자 하는 실행 프로그램이, 우리 빌드 시스템에서 빌드되지 않은, 그래서 $(ROOT_LIB_DIR)/$(OBJS_DIR) 밑에 없는 라이브러리(예를 들어 -pthread 나 -Xmu 같은)를 $(LIBS)에 지정하는 경우에는, 해당 타겟을 만들 규칙이 없다는 에러 메시지가 나오면서 링킹이 중단됩니다.

조금만 생각해 보면, 우리가 작성하지 않은, 그래서 이미 시스템에 설치된 (외부의) 라이브러리라면 굳이 의존성을 확인하지 않아도 된다고 볼 수 있지 않을까요? 또 그렇다면 라이브러리 의존성 규칙에 포함시킬 필요도 없는 것이고요.

결국 Makefile 에서 링킹할 라이브러리들을 우리 빌드 시스템에서 빌드된, 그래서 의존성 규칙에 포함시켜야 하는 것들과, 그렇지 않은 것들로 분리하고, 의존성 체크가 필요한 것들만 의존성 규칙에 포함시키는 것으로 처리하면 파일이 없다는 링킹 에러 문제를 피해갈 수 있습니다. 다음을 보세요.

의존성 확인이 필요한 라이브러리들은 Makefile에서 $(DEPEND_LIBS) 변수로 따로 모읍니다. 그리고 IncludePost.mk 파일에서는 $(ALL_LIBS) 에 $(DEPEND_LIBS)를 추가하고, 라이브러리 의존성 규칙에는 라이브러리들의 전체 파일 이름들로 치환한 결과를 추가하였습니다.

참고로 만약, 우리 빌드 시스템의 라이브러리가 아니지만, 이것과 비슷하게 라이브러리 의존성 규칙에 포함시킬 필요가 있다면, 그건 해당 Makefile에서 추가적으로 $(TARGET_NAMES) 규칙을 하나 더 처리하면 문제 없이 처리할 수 있습니다.

총정리

휴, 이제 대충 다 정리된 것 같으네요. 정리해 볼까요? 2개의 공용 include 파일들인 IncludePre.mk와 IncludePost.mk 과, 이를 사용하는 Makefile 의 원형인 MakeTemplate.mk 는 각각 다음과 같습니다.

새로운 모듈을 만들 때는 MakefileTemplate.mk 를 복사한 다음, 적절한 값으로 바꿔서 사용합니다. make의 실행 방법은 다음과 같습니다.

  • 사용 가능한 규칙
    • make (또는 make all): 현재 디렉토리 및 모든 서브 디렉토리들의 라이브러리와 실행 프로그램들 빌드
    • clean / cleanall: 현재 디렉토리의 결과물만 제거 / 현재 디렉토리 및 모든 서브 디렉토리들의 결과물 제거
    • depend / dependall: 현재 디렉토리의 의존성 파일 생성 / 현재 디렉토리 및 모든 서브 디렉토리드들의 의존성 파일 생성
    • lib / liball: 현재 디렉토리의 라이브러리 빌드 / 현재 디렉토리 및 모든 서브 디렉토리드들의 라이브러리 빌드
    • targets / targetsall: 현재 디렉토리의 실행 프로그램들 빌드 / 현재 디렉토리 및 모든 서브 디렉토리드들의 실행 프로그램들 빌드
    • 위 명령어에 RELEASE=1 을 넣으면 릴리즈 모드로, 그렇지 않으면 디버그 모드로 빌드
  • 전형적인 사용 예
    • 그냥 아무 생각없이 빌드하는 경우: make
    • 현재 디렉토리의 라이브러리만 빌드하려면: make lib
    • 실행 파일들을 빌드하는데 필요한 라이브러리들을 모두 빌드하려면: make liball
    • 현재 디렉토리의 실행파일들만(서브 디렉토리의 실행 파일들은 빼고) 빌드하려면: make liball targets

이번 글의 핵심은 make를 재귀호출해서 여러 개의 서브 모듈들을 한번에 빌드하는 것이었죠. 이 내용을 충분히 이해하신 분들이라면, 본인의 필요에 맞는 여러 가지 응용 규칙들을 만들어서 사용하실 수 있을 겁니다. 또한 Makefile의 서브 디렉토리들을 잘 엮어서, 최상위 디렉토리에서 필요한 라이브러리 및 실행 파일들을 한번에 빌드하는 것도 가능할 테고요.

이제서야 비로소 빌드 시스템이라고 부를만한 기본 틀을 갖추었네요. 앞으로는 여기에다 부가적인 기능들을 하나씩 추가해 보도록 하죠.

이 글과 관련된 글 목록:

This entry was posted in 소프트웨어 and tagged , , , , , , , , , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.