One DAG

recursive make 을 이용하면 각 모듈별로 makefile 을 작성하면 되기 때문에 프로젝트가 커져도 어려움 없이 makefile 을 관리할 수 있습니다. 그런데 recursive make 이 갖는 한가지 단점은 DAG 이 나누어져 있어서 모듈간 의존성을 처리하기 어렵다는 것입니다. 예를 들어 모듈 A 에서 빌드한 결과물 a 를 가지고 모듈 B 에서 b 를 빌드 하고 다시 그 결과물을 가지고 모듈 A 에서 c 를 빌드 하는 식으로 모듈간 의존성이 생길 수 있는데 recursive make 에서는 이와 같은 cross-makefile dependencies 를 처리하기가 어렵습니다.

이와 같은 전체 모듈간 의존성도 처리가 되려면 결국은 One DAG 그러니까 하나의 makefile 로 작성을 해야 됩니다. 그리고 기존의 recursive make 에서 모듈 별로 작성해 사용하던 makefile 은 include 파일로 변경해서 top makefile 에 include 합니다.

make 은 기본적으로 namespace 기능이 없기 때문에 모든 makefile 을 하나의 top makefile 에 include 한다면 이름 충돌을 피할 수가 없겠죠. 하지만 make 은 강력한 변수 기능과 makefile remake 기능이 있기 때문에 이것을 활용하면 쉽게 각 모듈별로 독립적인 변수 이름을 사용할 수가 있습니다.

먼저 변수 이름을 사용하는 방법을 살펴보면 디렉토리 dir1 에 위치한 모듈에서 사용하는 CFLAGS 변수는 CFLAGS.dir1 으로 작성하고 dir1/dir2 에 위치한 모듈에서 사용하는 CFLAGS 변수는 CFLAGS.dir1/dir2 로 작성해서 사용한다면 각 모듈별로 독립적인 CFLAGS 변수 이름을 사용할 수 있습니다. 하지만 변수 이름을 작성할 때마다 직접 디렉토리 접미사를 붙이는 것은 어렵기 때문에 makefile remake 기능을 이용해서 특정 스트링을 자동으로 각 모듈의 디렉토리 명으로 변경합니다. 예를 들어 makefile 에서 CFLAGS.$d 로 작성하기만 하면 remake 에 의해서 자동으로 .$d 접미사가 dir1dir1/dir2 로 변경되어 makefile 에 저장되고 최종 변경된 makefile 을 include 해 사용하면 각 모듈별로 독립적인 변수 이름을 사용하면서 One DAG 을 사용할 수 있게 됩니다.

Top Makefile

Top makefile 에서는 전체 모듈에서 사용할 수 있는 global 변수를 설정하고 remake 룰을 정의합니다. 각 모듈 디렉토리에서 사용자가 직접 작성하는 파일은 Makefile.mk 이고 remake 룰에 의해서 자동으로 생성되고 update 되는 파일은 앞에 dot 이 붙은 .Makefile.mk 가 됩니다. 이 dot 파일이 실제 include 지시자에 의해 읽혀지는 파일이 됩니다. remake 기능을 사용할 때 한가지 주의해야 될 점은 makefile 을 수정했는데 구문 에러가 발생해서 make 실행이 종료될 경우 Makefile.mk 파일을 다시 수정한다고 해서 오류가 없어지지 않는다는 점입니다. 왜냐하면 실제 include 되는 파일은 dot 파일이고 dot 파일은 remake 되기 전에 먼저 make 에의해 한번 읽혀지기 때문입니다. 따라서 이와 같은 경우는 먼저 d. ( delete-dot ) 타겟을 실행해주어야 합니다.

MAKEFLAGS += -rR
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all

CC   := gcc
AR   := ar
RM   := rm -f
AWK  := gawk
ARCH := x86_64

ifneq "$(release)" ""
  RELEASE := release
  CFLAGS  := -O2 -DNDEBUG
else
  RELEASE := debug
  CFLAGS  := -g -DDEBUG
endif

BUILD_DIR := BUILD/$(ARCH)-$(RELEASE)

#############################  (1)  #######################################

# 실제 .Makefile.mk 파일을 생성하는 함수로 Makefile.mk 파일에서 '.$d' 와 '$(d)' 스트링을
# regex 을 이용해 해당 디렉토리로 치환해서 .Makefile.mk 파일로 저장합니다.
# $1 : Makefile.mk
# $2 : .Makefile.mk
# $3 : diretory
define create_dot_makefile
echo "CREATE: $(strip $2)";                                              \
$(AWK) 'BEGIN {                                                          \
    while (getline < "$(strip $1)" > 0) {                                \
        print gensub( /\>(\.)\$$d\>|\$$\(d\)/, "\\1$(strip $3)", "g");   \
    }                                                                    \
}' > $2;
endef

# c. (create-dot) : 전체 모듈 디렉토리에서 .Makefile.mk 파일을 새로 생성합니다.
c. :
    @echo creating '.Makefile.mk' ...
    @set -e; $(foreach file, $(shell find * -name Makefile.mk), \
        $(call create_dot_makefile, \
            $(file), \
            $(patsubst %/Makefile.mk,%/.Makefile.mk,$(file)), \
            $(patsubst %/,%,$(dir $(file)))))
    @echo done.

# d. (delete-dot) : 전체 모듈 디렉토리에서 .Makefile.mk 파일을 일괄 삭제합니다.
d. :
    @echo removing '.Makefile.mk' ...
    @find -name '.Makefile.mk' -delete -printf 'DELETE: %p\n'
    @echo done.

###############################  (2)  #################################

# remake rule 을 정의하는 것으로 Makefile.mk 이 수정되면 자동으로 .Makefile.mk 가 갱신됩니다.
%.Makefile.mk : %Makefile.mk
    @$(call create_dot_makefile,$<,$@,$(@D))

ALL    :=
CLEAN  :=
# c. d. 타겟을 실행할 때는 include 하지 않습니다.    
ifeq "$(filter c. d.,$(MAKECMDGOALS))" ""
# 실제 include 되는 파일은 dot 파일이므로 .Makefile.mk 로 subst 합니다.
-include $(foreach file, \
            $(shell find * -name Makefile.mk),  \
            $(subst Makefile.mk,.Makefile.mk,$(file)))
endif

########################################################################

all   : $(ALL)    
clean : $(CLEAN)
.PHONY: all clean c. d.

모듈 Makefile.mk

다음은 사용자가 각 모듈 소스 디렉토리에서 작성하는 makefile 예제입니다. 먼저 remake 룰에 의해 자동으로 디렉토리 명으로 치환되는 스트링은 이름 뒤에 접미사로 사용되는 .$d 와 독립적으로 쓸 수 있는 $(d) 두가지 입니다. top makefile 이 위치한 곳으로부터 dir1/dir2 하위 디렉토리에서 Makefile.mk 을 작성한다고 할경우 $(d) 값은 dir1/dir2 가 됩니다. 다시 말해서 $(d) 는 소스 디렉토리 경로가 됩니다. 만약에 다른 모듈에서 정의된 변수를 사용할 필요가 있을 때는 $(AA.dir1/dir2) 와 같이 직접 디렉토리명을 작성하거나 아니면 AA.$d := $(AA.dir1/dir2/dir3) 와 같이 먼저 해당 모듈 변수로 대입한 후 사용하면 됩니다.

.$d$(d) 는 단순 regex 치환이므로 다른 곳에도 사용되지 않도록 주의해야 합니다.

BDIR.$d   := $(BUILD_DIR)/$(d)
CFLAGS.$d := $(CFLAGS) -MMD -MP -I. -Wall

#######################  (1)  ###########################

EXES.$d := $(addprefix $(BDIR.$d)/, \
        change_case fifo_seqnum_client fifo_seqnum_server \
        pipe_ls_wc pipe_sync popen_glob simple_pipe)
OBJS.$d := $(addsuffix .o,$(EXES.$d))
DEPS.$d := $(OBJS.$d:.o=.d)

all.$d : $(EXES.$d)

TLPI_LIB.$d := $(TLPI_LIB.lib)   # 다른 모듈에서 정의된 변수를 .$d 변수에 대입해 사용

########################  (2)  ###########################

$(BDIR.$d) : ;@mkdir -p $@

$(OBJS.$d) : $(BUILD_DIR)/%.o : %.c | $(BDIR.$d)
    $(CC) -c $(CFLAGS.$d) -o $@ $<

$(EXES.$d) : $(TLPI_LIB.$d)      # 다른 모듈에 존재하는 파일과 의존관계 설정
$(EXES.$d) : % : %.o
    $(CC) $(LDFLAGS.$d) $+ $(LDLIBS.$d) -o $@

########################  (3)  ############################

ifeq "$(filter clean clean.%,$(MAKECMDGOALS))" ""
  -include $(DEPS.$d)
endif

clean.$d :
    rm -f $(BDIR.$d)/*

ALL   += all.$d
CLEAN += clean.$d

.PHONY: all.$d clean.$d

모듈 Makefile.mk 에서 INCLUDE 의 사용

위와 같은 모듈 makefile 을 작성하다 보면 독립적인 변수를 사용할 수 있어서 좋기는 한데 한가지 단점이 다른 모듈들의 makefile 과 중복되는 부분이 많다는 것입니다. 위 makefile 을 예로 들면 비슷한 구조의 모듈일 경우 위에서 (1) 번 부분을 제외하면 모두 공통으로 중복되는 코드들입니다. 이럴 경우 (1) 번만 남기고 나머지는 include 로 빼면 좋은데 여러 모듈에서 include 해서 사용하는 makefile 의 경우는 $(d) 처리를 위해서 remake 룰을 적용하기가 어렵습니다 ( 왜냐하면 각 모듈별로 dot 파일을 생성해야 되기 때문에 ). 따라서 이때는 top makefile 에 정의되어 있는 remake 룰에서 include 도 함께 처리합니다. 이때는 make 의 include 지시자와 구분하기 위해 대문자 INCLUDE 를 사용합니다. 적용하고 나면 다음과 같이 해당 모듈에서 필요한 부분만 남게 됩니다.

INCLUDE mk/header.mk

EXES.$d := $(addprefix $(BDIR.$d)/, \
        change_case fifo_seqnum_client fifo_seqnum_server \
        pipe_ls_wc pipe_sync popen_glob simple_pipe)
OBJS.$d := $(addsuffix .o,$(EXES.$d))
DEPS.$d := $(OBJS.$d:.o=.d)

all.$d : $(EXES.$d)

CFLAGS.$d   += -Wall
TLPI_LIB.$d := $(TLPI_LIB.lib)

INCLUDE mk/body.mk 
INCLUDE mk/footer.mk

예제를 위해서 INCLUDE 파일들은 header.mk, body.mk, footer.mk 로 구분하여 mk 디렉토리에 저장했습니다. 한가지 INCLUDE 파일을 사용할 때 주의할 점은 이 파일들은 remake 룰에서 awk 에의해 직접 처리되기 때문에 Makefile.mk 와 달리 자동으로 update 체킹이 안됩니다. 따라서 mk 디렉토리에 있는 INCLUDE 파일을 수정했을 경우에는 빌드전에 c. ( create-dot ) 타겟을 한번 실행해주어야 합니다.

sh$ cat mk/header.mk 

BDIR.$d   := $(BUILD_DIR)/$(d)
CFLAGS.$d := $(CFLAGS) -MMD -MP -I.
---------------------------------------------------
sh$ cat mk/body.mk 

$(BDIR.$d) : ;@mkdir -p $@

$(OBJS.$d) : $(BUILD_DIR)/%.o : %.c | $(BDIR.$d)
    $(CC) -c $(CFLAGS.$d) -o $@ $<
. . . .
---------------------------------------------------
sh$ cat mk/footer.mk 

ifeq "$(filter clean clean.%,$(MAKECMDGOALS))" ""
  -include $(DEPS.$d)
endif
. . . .

Top Makefile 의 경우에는 create_dot_makefile 함수만 다음과 같이 변경해 주면 됩니다.

define create_dot_makefile
echo "CREATE  $(strip $2)";                                                  \
$(AWK) 'BEGIN { readf( "$(strip $1)" ) }                                     \
function readf( file,   i, z ) { included[file];                             \
    while (getline < file > 0) {                                             \
        $$0 = gensub( /\>(\.)\$$d\>|\$$\(d\)/, "\\1$(strip $3)", "g");       \
        if ( /^\s*INCLUDE\s+.+\.mk\s*$$/ ) { z = $$0;                        \
            for ( i=2; i <= NF; i++ )                                        \
                if (!( $$i in included )) { readf( $$i ); $$0 = z }          \
        } else print                                                         \
    }                                                                        \
    if (ERRNO) { print "ERROR  " file " : " ERRNO > "/dev/stderr"; exit 1 }  \
}' > $2;
endef

테스트용 예제 파일

예제 프로젝트 one_DAG 은 빌드시 아래와 같은 방법들을 사용할 수 있습니다.

  1. makefile 수정후 구문 에러로 종료될 경우 먼저 d. ( delete-dot ) 실행후 해당 makefile 수정
  2. INCLUDE 에서 사용되는 makefile 은 수정후 c. ( create-dot ) 실행
  3. .$d$(d) 스트링이 다른 곳에도 사용되지 않도록 주의

예제파일 다운로드: one_DAG.tar.gz

# debug 빌드 (default)          # release 빌드
sh$ make all                    make all release=1                # 전체 빌드
sh$ make all.pipes              make all.pipes release=1          # pipes 만 빌드
sh$ make all.sub/sockets        make all.sub/sockets release=1    # sub/sockets 만 빌드

sh$ make clean                  make clean release=1              # 전체 clean
sh$ make clean.pipes            make clean.pipes release=1        # pipes 만 clean
sh$ make clean.sub/sockets      make clean.sub/sockets release=1  # sub/sockets 만 clean

sh$ make shared=1  # shared library 도 빌드

sh$ make d.        # delete-dot : 전체 .Makefile.mk 파일 삭제
sh$ make c.        # create-dot : 전체 .Makefile.mk 파일 새로 생성

예제 파일 작성을 위해서 The Linux Programming Interface ( http://man7.org/tlpi/code/index.html ) 책의 소스코드가 사용되었습니다. ( 테스트용이므로 컴파일러 warning 은 무시하세요 )