ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [빌드] Makefile
    프로젝트/운영체제 만들기 2023. 8. 3. 13:17

    글의 참고

    - https://modoocode.com/311

    - https://stackoverflow.com/questions/17834582/run-make-in-each-subdirectory

    - https://stackoverflow.com/questions/11184389/what-does-wildcard-mean-in-makefile

    - https://www.gnu.org/software/make/manual/make.html

    - https://www.gnu.org/software/make/manual/html_node/File-Name-Functions.html


    글의 전제

    - 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다.

    - 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다.


    글의 내용

    - 기본 형태

    " make의 기본형태는 아래와 같다. make에서 아래와 같은 `target`을 기준으로 액션을 수행하게 되는데, 이렇게 여러 개의 `target` 들이 모이면 `rules`을 구성한다고 한다.

    target … : prerequisites …
        recipe … …

     

     

    " `target` 은 대개 makefile을 통해서 생성되는 실행 파일 및 오브젝트 파일을 의미한다. `target`은 그저 `액션`의 용도로만 쓰일 경우도 있다. 즉, 특정 파일을 만들지 않고 그저 어떤 일을 수행하고 싶을 때도 `target`을 명시할 수 있다. 예를 들어, `clean` 같은 것들이 있다. 이럴 때, `clean`이라는 파일을 만들 수 있기 때문에 makefile에서 지원해주는 `PHONY` 라는 키워드를 사용해서 파일이 생성되는 것을 막을 수 있다. 

     

    " `prerequisites`는 `target` 파일을 만들기 위해 필요한 입력 파일들 이라고 보면 된다. 즉, `의존성`이라고 보면 된다. 마지막으로 `recipe`는 `target`이 수행할 액션(커맨드)을 의미한다. 이건 `GNU makefile` 공식 문서를 참고해서 해서 보자.

    ...
    A recipe is an action that make carries out. A recipe may have more than one command, either on the same line or each on its own line. Please note: you need to put a tab character at the beginning of every recipe line! This is an obscurity that catches the unwary. If you prefer to prefix your recipes with a character other than tab, you can set the .RECIPEPREFIX variable to an alternate character (see Other Special Variables).

    Usually a recipe is in a rule with prerequisites and serves to create a target file if any of the prerequisites change. However, the rule that specifies a recipe for the target need not have prerequisites. For example, the rule containing the delete command associated with the target ‘clean’ does not have prerequisites.
    ...

     

    " 모든 레시피는 반드시 `tab`을 하나 넣고 시작해야 한다. `tab`을 넣지 않으면, 해당 레시피는 레시피라고 인식되지 않는다. 그저 전역적으로 사용되는 명령어라고 인식된다. 그리고 위에서 말했던 것처럼, 반드시 `target`이 생성하기 위한 파일일 필요는 없다.

     

    - 패턴 사용하기

    " 우리의 경우 파일이 3 개 밖에 없어서 다행이였지만 실제 프로젝트에는 수십~ 수백 개의 파일들을 다루게 될 것입니다. 그런데, 각각의 파일들에 대해서 모두 빌드 방식을 명시해준다면 Makefile 의 크기가 엄청 커지겠지요. 다행이도 Makefile 에서는 패턴 매칭을 통해서 특정 조건에 부합하는 파일들에 대해서 간단하게 recipe 를 작성할 수 있게 해줍니다.

    foo.o : foo.h foo.cc
    	$(CC) $(CXXFLAGS) -c foo.cc
    
    bar.o : bar.h bar.cc
    	$(CC) $(CXXFLAGS) -c bar.cc

     

     

    " 일단 먼저 비슷하게 생긴 위 두 명령들을 어떻게 하면 하나로 간단하게 나타낼 수 있는지 보겠습니다.

    %.o: %.cc %.h
    	$(CC) $(CXXFLAGS) -c $<

     

     

    " 먼저 %.o 는 와일드카드로 따지면 마치 *.o 와 같다고 볼 수 있습니다. 즉, .o 로 끝나는 파일 이름들이 타겟이 될 수 있겠지요. 예를 들어서 foo.o 가 타겟이라면 % 에는 foo 가 들어갈 것이고 bar.o 의 경우 % 에는 bar 가 들어갈 것입니다.

    따라서 예를 들어 foo.o 가 타겟일 경우

    foo.o: foo.cc foo.h
    	$(CC) $(CXXFLAGS) -c $<

     

     

    " 가 됩니다. 참고로 패턴은 타겟과 prerequisite 부분에만 사용할 수 있습니다. recipe 부분에서는 패턴을 사용할 수 없습니다. 따라서 컴파일러에 foo.cc 를 전달하기 위해서는 Makefile 의 자동 변수를 사용해야 합니다.

    $< 의 경우 prerequisite 에서 첫 번째 파일의 이름에 대응되어 있는 변수 입니다. 위 경우 foo.cc 가 되겠지요. 따라서 위 명령어는 결과적으로

    foo.o: foo.cc foo.h
    	$(CC) $(CXXFLAGS) -c foo.cc

     

     

    " 가 되어서 이전의 명령어와 동일하게 만들어냅니다.

    Makefile 에서 제공하는 자동 변수로는 그 외에도 아래 그림과 같이 $@, $<, $^ 등등이 있습니다.

    • $@ : 타겟 이름에 대응됩니다.
    • $< : 의존 파일 목록에 첫 번째 파일에 대응됩니다.
    • $^ : 의존 파일 목록 전체에 대응됩니다.
    • $? : 타겟 보다 최신인 의존 파일들에 대응됩니다.
    • $+ : $^ 와 비슷하지만, 중복된 파일 이름들 까지 모두 포함합니다.

    하지만 애석하게도 위 패턴으로는

    main.o : main.cc foo.h bar.h
    	$(CC) $(CXXFLAGS) -c main.cc
    

     

    " 를 표현하기에는 부족합니다. 왜냐하면 의존 파일 목록에 main.h 가 없고 foo.h  bar.h 가 있기 때문이죠. 사실 곰곰히 생각해보면 이 의존파일 목록에는 는 해당 소스 파일이 어떠한 헤더파일을 포함하냐에 결정되어 있습니다. main.cc  foo.h  bar.h  include 하고 있기 때문에 main.o  prerequisite  main.cc 외에도 foo.h  bar.h 가 들어가 있는 것입니다.

     

    물론 매번 이렇게 일일히 추가할 수 있겠지만, 소스 파일에 헤더 파일을 추가할 때 마다 Makefile 을 바꿀 수는 없는 노릇이니까요. 하지만 다행이도 컴파일러의 도움을 받아서 의존파일 목록 부분을 작성할 수 있습니다.

     

    : %

    " %는 *과 거의 동일하다. 근데 mkaefile %는 좀 더 특이한 점이 있다. 바로 한 시점에, 딱 하나에만 대응된다는 것이다.

    Here are some examples of pattern rules actually predefined in make. First, the rule that compiles ‘.c’ files into ‘.o’ files:
    %.o : %.c
            $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
    defines a rule that can make any file x.o from x.c. The recipe uses the automatic variables ‘$@’ and ‘$<’ to substitute the names of the target file and the source file in each case where the rule applies
    ....

    - 참고 : https://www.gnu.org/software/make/manual/html_node/Pattern-Examples.html [ 10.5.2 Pattern Rule Examples ]
    SRCS:=$(wildcard *.c)
    OBJS:=$(SRCS:%.c=%.o)
    
    $(OBJS): $(SRCS)  
        gcc -c $< -o $@
        
    cpr :
        gcc -c $(SRCS) -o $(OBJS)
    
    %.o: %.c  
        gcc -c $< -o $@

     

    " 첫 번째, 두 번째 문장은 에러를 발생시키는 문장이다. 둘의 특징은 한 번에 모든 소스 파일을 대응하는 모든 오브젝트 파일로 만들려는 것이다. 예를 들면, 다음과 같다.

     

    gcc -c 1.c 2.c 3.c -o 1.o 2.o 3.o

     

     

    " 이런 문장을 오류를 일으킨다. 그렇면, 두 번째 문장을 이것을 어떻게 해석할까? 위의 GNU Make 공식 문서에는 `%` 은 `룰이 적용되는 각각의 케이스`라고 정의하고 있다. `%`은 해당 타겟을 여러 번 돌리는 것과 같다. 

    gcc -c 1.c -o 1.o
    gcc -c 2.c -o 2.o
    gcc -c 3.c -o 3.o

     

     

    - 함수

    : wildcard < $(wildcard pattern) >

    " wildcard의 형식은 '$(wildcard <pattern>)` 으로 사용되며, 패턴에 정의된 파일 포맷에 맞는 파일들을 목록을 불러오는데 사용된다. 

    ...
    SRCS = $(wildcard ./*.c)
    ...

     

    " 현재 폴더에 1.c, 2.c, 3.c가 있다면 SRCS안에는 1.c, 2.c, 3.c가 들어가게 된다. 

     

     

     

    : substitution reference

    ...
    SRCS = $(wildcard *.c)
    OBJS = $(SRCS:.c=.o)
    ...

     

    " 위의 패턴으로 상당히 자주 쓰인다. GNU make 공식사이트에서는 해당 내용을 `앞에 명시된 확장자를 뒤에 명시된 확장자로 바꾸고 나서, 파일의 이름을 가져온다` 정도로 해석이 되어 있다. 결론적으로, SRCS가 1.c, 2.c, 3.c라면 OBJS는 1.o, 2.o, 3.o가 된다.

     

    " substitution reference 이, `patsubst` 함수를 완전히 대체한다고 보기는 어렵다.

     

    : patsubst

    " 예시로 알아보자.

    ...
    SRCS = $(patsubst %.c, %.o, ec.c.c ad ef.c ec.o ed.c)
    ...

     

    " $(SRCS)에는 `ec.c.o ad  ef.o ec.o  ed.o` 목록이 있게 된다.

     

     

    : notdir

    " 예시로 알아보자.

    NOT_DIRS = $(notdir TEST/KERNEL/apple.c TEST/banana.c output.c)

     

    " 위와 같은 경우, $(NOT_DIRS)은 `apple.c banana.c output.c` 목록을 갖는다. 

     

     

    : filter-out [참고1]

    " filter-out 함수는 패턴에 속하는 문자열들을 제외한 sub-string 을 반환한다. 포맷은 다음과 같다. 예시도 보면 바로 이해할 수 있을 것이다.

    # Format
    $(filter-out pattern...,text)
    
    # Example
    objects=yohda1.o foo.o yohda2.o bar.o	
    	mains=yohda1.o yohda2.o		
    	$(filter-out $(mains),$(objects)) # yohdas 에 있지 않는 *.o(foo.o bar.o) 를 담고 있는 리스트를 생성한다.

     

     

     

    : addprefix < $(addprefix prefix,names…) >

    " 두 번째 인자인 `names`에 들어간 문자열들에 `prefix`로 전달되는 값을 앞쪽에 추가한다. 예를 들어, 아래 예시처럼, 두 번째 인자로 `foo bar`가 전달되면, 먼저 `띄어쓰기`를 기준으로 분리한다. 그리고, 각 구분된 단어앞에 `src/(prefix)`를 붙힌다. 결과는 아래 보이는 것처럼 `src/foo src/bar`가 된다.

    The argument `names` is regarded as a series of names, separated by `whitespace`; prefix is used as a unit. The value of prefix is prepended to the front of each individual name and the resulting larger names are concatenated with single spaces between them.

    $(addprefix src/,foo bar)

    For example,produces the result ‘src/foo src/bar’.

     

     

    - origin [참고1]

    " 이 함수는 값 자체에는 관심이 없다. 해당 값이 어디서 온 것인지에 대해서만 관심이 있다. 예를 들어, 아래의 코드를 보자. 아래 Makefile 에서 $(origin V) 은 `V` 변수가 어디에서 왔는지를 문자열로 반환한다.

    # Makefile
    ifeq ("$(origin V)", "command line")
    VERBOSE=$(V)
    endif
    ifndef VERBOSE
    VERBOSE=0
    endif

     

     

    " 예를 들어, 아래와 같이 shell 에서 `make V=2` 를 입력하면, $(origin V) 는 V 가 어디서 전달되었는지를 체크한다. 이 때, shell(command line interface) 에서 왔기 때문에, $(origin V) 는 "command line" 을 반환하게 된다. 만약, shell script 내에서 아래 `make V=2` 를 실행하면 어떻게 될까? "command line"  를 반환하게 된다. 만약, V 가 Makefile 파일내에서 정의된 변수라면, "file" 을 반환하게 된다.

    yohda$ make V=2

     

     

     

     

    - 변수 할당

    - `?=`

    " `?=` 오른쪽에 있는 변수가 `undefined`일 경우에만, 적용된다. 아래의 예시에서 `FOO ?= bar`에서 bar가 정의되어 있지 않은 경우, `FOO` 변수에는 `bar`라는 문자열이 할당된다. 만약, `bar`변수는 정의되어 있는데, 값이 비어있는 경우라면, 그건 정의가 이미 되어있는 것이므로, `FOO`에는 아무값도 할당되지 않는다.

    There is another assignment operator for variables, ‘?=’. This is called a conditional variable assignment operator, because it only has an effect if the variable is not yet defined. This statement:

    FOO ?= bar
    is exactly equivalent to this (see The origin Function):

    ifeq ($(origin FOO), undefined)
        FOO = bar
    endif
    Note that a variable set to an empty value is still defined, so ‘?=’ will not set that variable.

    - 참고 : https://www.gnu.org/software/make/manual/make.html [ 6.2.4 Conditional Variable Assignment ]

     

     

    : `=` 

    " 이 연산자는 할당 시점에는 값이 정해지지 않는다. 예를 들어, 아래 `foo` 변수는 `bar` 변수의 값이 아닌, 참조를 갖게된다. `foo` 변수에는 `123`이 있을 거 같지만, `123` `456` 이라는 값이 있게된다. 왜냐면, `bar` 변수의 참조를 가지고 있기 때문에, `bar` 변수의 최종갑이 `foo` 변수에 할당된다. 이런 `재귀 할당자`의 단점은 무한 루프를 발생시킬 수 있다는 것이다. 아래에서 `CFLAGS = $(CLFAGS) -O`는 자기 자신을 참조하게 되서 무한루프가 발생한다.

    The first flavor of variable is a recursively expanded variable. Variables of this sort are defined by lines using ‘=’ (see Setting Variables) or by the define directive (see Defining Multi-Line Variables). The value you specify is installed verbatim; if it contains references to other variables, these references are expanded whenever this variable is substituted (in the course of expanding some other string). When this happens, it is called recursive expansion. For example,

    bar = '123'
    foo = $(bar)
    ...

    bar += '456'
    ...

    CFLAGS = $(CFLAGS) -O

    - 참고 : https://www.gnu.org/software/make/manual/make.html [ 6.2.1 Recursively Expanded Variable Assignment ]

     

     

    : `:=`, `::=`

    " 할당되는 시점에 값으로 고정된다. 만약, 아래 예시에서 `=` 연산자를 사용했다면, `y`는 `later bar`가 됬을 것이다. 대신에 `:=`, `::=`을 사용함으로써, 할당되는 시점에 값이 고정해서 `y`는 최종적으로 `foo bar`가 된다. `:=`와 `::=`는 사실상 기능적으로 차이가 없기때문에 2개를 혼용해서 사용해도 된다. 

    The value of a simply expanded variable is scanned once, expanding any references to other variables and functions, when the variable is defined. Once that expansion is complete the value of the variable is never expanded again: when the variable is used the value is copied verbatim as the expansion. If the value contained variable references the result of the expansion will contain their values as of the time this variable was defined. Therefore,
    x := foo
    y := $(x) bar
    x := later
    is equivalent to

    y := foo bar
    x := later
    ...

    - 참고 : https://www.gnu.org/software/make/manual/make.html [ 6.2.2 Simply Expanded Variable Assignment ]

     

     

     

    - define

    " define 문은 call 명령어와 함께 굉장히 자주 사용된다. 사용하는 포맷은 다음과 같다. Makefile 에서 define 으로 정의된 심볼은 일반 Makefile 의 변수인 매크로 변수와 동일하다고 한다. 그런데, 실무에서 define 및 call 을 사용하는 케이스들을 보면 인자에 따라 동적으로 target 을 만들기 위해서 주로 사용된다. 예를 들면, 아래와 같다.

    # Format
    define Build
    	....
    endef
    
    
    # Example
    define temp_target
      $(info "[yohda][teset]")
      $(1)/stamp-$(3):=$(if $(6),$(6),$(STAGING_DIR))/yohda/.$(2)_$(3)$(5)
      $$($(1)/yohda-$(3)): $(TMP_DIR)/.build $(4)
      	#@echo "[yohda][build/subdir.mk][$(1)/yohda-$(3)]"
    	@+$(SCRIPT_DIR)/timeyohda.pl -n $$($(1)/stamp-$(3)) $(1) $(4) || \
    		$(MAKE) $(if $(QUIET),--no-print-directory) $$($(1)/flags-$(3)) $(1)/$(3)
    	@mkdir -p $$$$(dirname $$($(1)/yohda-$(3)))
    	@touch $$($(1)/yohda-$(3))
    
      $$(if $(call debug,$(1),v),,.SILENT: $$($(1)/yohda-$(3)))
    
      .PRECIOUS: $$($(1)/yohda-$(3)) # work around a make bug
    
      $(1)//clean:=$(1)/yohda-$(3)/clean
      $(1)/yohda-$(3)/clean: FORCE
    	@rm -f $$($(1)/yohda-$(3))
    
    endef

     

     

    " define 문에서 주의 사항은 여러 가지가 있지만 나같은 경우는 출력문에서 문제가 발생했다. 왜냐면, define 은 그대로 치환이라고 생각하면 target 안에서는 `@echo` 를 사용하고, target 밖에서는 `$(info ...)` 를 사용해야 하는데, define 문에서만 사용가능한 출력 방법이 있는주 알고 구글에 검색하고 다녔다. 

     

     

     

     

    - 하위 디렉 토리

    " make에서 재귀적으로 하위 디렉토리들을 실행하는 것이 쉬운 일은 아닌 것 같다. 스택 오버플로우에서 꾀나 논쟁이 있는것 같은데, 나는 GNU make에서 지언하는 `.PHONE`를 이용해서 하는 방법을 써 볼까 한다.

    SUBDIRS = $(wildcard */.) 
    
    all: $(SUBDIRS) post
    
    $(SUBDIRS):
    	make -C $@		
    
    .PHONY: $(SUBDIRS)

     

     

    " 주의할 점이 있다. 서브 디렉토리를 재귀적을 돌 때, 해당 디렉토리에 makefile이 없으면 에러를 뿜고 종료한다. 그리고 $(wildcard */.) 은 바로 한 단계 하위 디렉토리만 불러온다. 즉, 하위 디렉토리에서 다시 위의 구조를 재사용 해야 한다.

     

     

    - 문자 출력

    " 아래의 코드를 참고하자.

    ...
    SRCS = $(wildcard *.c)
    OBJS = $(SRCS:.c=.o) 
    ...
    ...
    
    $(info $(SRCS))                                                   
    $(info $(OBJS))
    ...
    ...

     

     

    " $(SRCS)과 $(OBJS)에 들어있는 모든 파일들의 이름을 출력한다. 주의할 점이 있다. $(info ...) 를 사용할 때는 개행 및 띄어쓰기는 하지 않는 것으로 보인다.

    ifeq (x$(FILE_EXIST),xno)
    $(info yohda) 		### sccuess
    	$(info yohda) 	### error
    endif

     

     

    " 그러나, 주의할 점이 있다. 아래의 차이점이 뭘지 생각해보자. 예를 들어, shell 에서 `make` 명령어를 실행했을 때, 실행되는 출력문은 하나가 있다. 어떤 문자열이 출력될까? 바로 `$(info yohda22)` 다. $(info ...) 문은 target 에 구애받지 않고, Makefile 에서 전역적으로 사용이 가능하다. 그러나, 특정 target 안에 작성된 `@echo "printdb"` 는 반드시 `make printdb` 처럼 printdb 타겟이 실행되어야만 출력되는 문장이다.  

    ....
    printdb:
    	@true
    	@echo "printdb"
    $(info yohda22)
    ....

     

     

     

    - 옵션

    : make -C ${DIR} [참고1]

    " 해당 디렉토리로 이동하는 것과 같다. 그리고 해당 디렉토리안에 makefile을 실행한다. 즉, recursion 이라고 보면 된다. background 로 실행되지 않고 foreground 로 실행된다. 즉, 순차적 실행이다.

    SUBDIRS = $(wildcard */.) 
    
    all: $(SUBDIRS) post
    
    $(SUBDIRS):
    	make -C $@		
    
    .PHONY: $(SUBDIRS)

     

    " $(wildcard */.)을 통해 현재 디렉토리의 한 단계 아래의 디렉토리들의 목록을 받는다. 그리고 `$(SUBDIRS): make -C $@`를 사용해서 모든 서브 디렉토리들의 makefile을 실행하게 된다. 

     

    " .PHONE를 사용한 이유는 makefile 파일은 기본적으로 target을 결과물로 만들려고 한다. 즉, $(SUBDIRS)들을 결과물로 만든다고 할려는 것이다. 그런데 우리의 목적은 서브 디렉토리를 결과물로 만들려고 하는것이 아니기 때문에, .PHONE를 사용한다.

     

     

    : make -f ${FILE_NAME} "

    " makefile의 이름을 직접 지정해서 실행할 수 있다. 예를 들어, 아래의 내용을 보자.

    // test
    {
    SUBDIRS = $(wildcard */.) 
    
    all: $(SUBDIRS) post
    
    $(SUBDIRS):
    	make -C $@		
    
    .PHONY: $(SUBDIRS)
    }

     

    " 위의 파일의 이름은 `test` 이다. 근데 내용이 makefile이다. 그러므로, 이 파일을 makefile로 쓰고 싶다면, `make -f test`라고 실행하면 된다.

     

     

    : make -w [참고1]

    " `-w` 옵션을 사용할 경우, `make -C` 명령을 실행할 때 `make[n] : Entering directory...` 및 `make[n]: Leaving directory ...` 로그를 출력한다.

     

    - Directive

    - include [참고1 참고2]

    " 포맷은 다음과 같다.

    - include filenames…

     

    " 현재 실행중인 Makefile을 읽는 것을 멈추고, 파라미터로 전달된 Makefile 들을 모두 읽고나서 다시 실행한다. 만약, <filenames> 가 비어있다면 아무일도 하지 않는다. 심지어 에러 로그도 뿌리지 않다. <filenames> 에는 패턴이 적용될 수 있다. 그런데, 이건 `-include <filenames>` 로 사용할 때다. `include build/Makefile` 처럼 사용할 경우, 이건 반드시 `build/Makefile` 이 존재해야 한다.

     

    include foo *.mk -> include foo a.mk b.mk c.mk

     

     

    - override [참고1]

     

     

    - 특수 변수

    : MAKECMDGOALS

    " MAKECMDGOALS 은 make 에서 내부적으로 사용하는 변수로 make 수행 시 전달되는 target list 를 저장한다. 그렇다면, MAKECMDGOALS 와 $@ 의 차이는 뭘까

     

     

    : MAKEFILE_LIST

    " include 에 포함된 Makefile 파일들을 출력한다.

     

     

     

    - Makefile 파일에서 @echo 와 echo 의 차이 [참고1]

    " Makefile 파일 target 안에서 주로 @command 를 사용한다. 그런데, 어떤 코드를 보면 그냥 사용하는 경우가 존재한다. 그렇다면 무슨차이가 존재할까? 예를 들어, 아래와 같이 `yohda` 라는 타겟이 존재한다고 가정하자. 결과만 보면 `echo "yohda"` 는 접두사로 @ 를 사용하지 않아서 shell 에 실행할 명령어를 출력한 뒤 명령어를 실행한다. 그러나, `@echo "yohda2"` 는 실행할 코드를 출력하지 않고 즉각적으로 실행한다.

    # Makefile
    yohda:
        echo "yohda"
        @echo "yohda2"
        
    $ make
    echo "yohda1"
    yohda1
    yohda2

     

     

     

    - Dependency [참고1]

    " Makefile 파일의 의존성은 대개 헤더 파일과 관련이 있다. Makefile은 기본적으로 소스 파일의 변화는 인식하지만, 헤더 파일의 변화는 인식하지 못한다. 이러한 부분을 Makefile이 아닌, GCC를 통해서 해결이 가능하다. GCC는 컴파일되는 각각의 소스 파일이 참조하는 헤더 파일들을 모조리 긁어온다. 그리고 나서 해당 내용을 토대로 `의존성 파일`이라는 것을 만들어 준다. 이 파일은 Makefile이 실행할 수 있는 형태인 TARGET, DEPENDENCY, RECIPE 구조를 갖추고 있다. 그래서 이 형태를 Makefile 파일에 포함시켜서 해당 내용을 Makefile이 실행할 수 있도록 만드는 것이다. 이때, 필요한 지시어가 `include` 지시어다. `include` 지시어는 해당 지시어를 만나면, 현재 진행 프로세스를 멈추고 해당 Makefile로 들어가서 모두 실행하고 나서 다시 돌아온다.

     

     

     

    - 매크로

    : ifeq & ifdef

    " ifeq는 if-else 형태의 구성이 가능하다. 대신, 2개의 값을 비교하는 문법이기 때문에 2개의 인자를 받는다. 간단한 IF문을 구성하고 싶다면, 두 번째 `ifdef`를 추천한다. 아래의 makefile의 파라미터와 함께 여러 타겟 및 디버깅용으로 주로 사용한다.

    ifeq ($(TARGET_CPU),x86)
      TARGET_CPU_IS_X86 := 1
    else ifeq ($(TARGET_CPU),x86_64)
      TARGET_CPU_IS_X86 := 1
    else
      TARGET_CPU_IS_X86 := 0
    endif
    
    ...
    ...
    
    ifdef $(HAVE_CLIENT)
    libtest_LIBS = \
        $(top_builddir)/libclient.la
    else
    ifdef $(HAVE_SERVER)
    libtest_LIBS = \
        $(top_builddir)/libserver.la
    else
    libtest_LIBS = 
    endif

     

     

    " `타겟` 안에 조건문을 작성할 수 있다. 그런데, 주의점이 있다. 왼쪽은 안되고, 오른쪽은 된다. 그 이유는 makefile의 `타겟`안에서는 들여쓰기를 적용할 경우, 무조건 `레시피`로 인식이된다. 그래서 왼쪽은 `ifeq`를 레시피로 인식해서 컴파일 오류가 난다.

    pre:
        mkdir -p $(OBJ_DIR)
        ifeq (${SUB}, 1)
            $(info "continue to build 16-bit")
        else
            $(info "only to build 32-bit")                                                                
        endif
    pre:
        mkdir -p $(OBJ_DIR)
    ifeq (${SUB}, 1)
        $(info "continue to build 16-bit")
    else
        $(info "only to build 32-bit")                                                                
    endif

     

     

    - 인자 전달

    " 아래의 예제를 `make ${PARAM_NAME}=${PAREM_VALUE}` 형태로 해서 makefile 파일안으로 값을 전달할 수 가 있다. 아래 예제는 gcc에 옵션을 동적으로 삽입하기 위해 사용했다.

    $ cat Makefile 
    all:
        echo foo | gcc $(USER_DEFINES) -E -xc - 
    
    $ make USER_DEFINES="-Dfoo=one"
    echo foo | gcc -Dfoo=one -E -xc - 
    ...
    one
    
    $ make USER_DEFINES="-Dfoo=bar"
    echo foo | gcc -Dfoo=bar -E -xc - 
    ...
    bar
    
    $ make 
    echo foo | gcc  -E -xc - 
    ...
    foo

     

     

    - 빌드 시스템 구축

    " 커널을 만들 때, 폴더가 여러 개면 빌드 시스템이 상당히 복잡해진다. 참조 글 통해서 나만의 빌드 시스템을 약간 이나마 구현할 수 있을 듯 하다. 그리고, 대개 큰 프로젝트들은 `make` 를 실행하기 전에 `setup.sh` 혹은 `build.sh` 같은 쉘 스크립트를 실행한다. 그 이유는 여러 가지가 있을 수 있지마, 전역적인 환경 변수를 설정하기 위해서 일 것이다. `Makefile` 파일안에서 생성하는 환경 변수는 해당 파일안에서만 사용이 가능하다. `Makefile` 이 실행되면, 스레드가 하나 실행되는 것과 같고 그 스레드안에서 선언한 환경 변수만 사용 가능하기 때문에 다른 `Makefile` 안에서는 접근이 안된다. 근데, 쉘 스크립트는 전역 환경  변수가 설정이 가능하다. 

     

    - 테크닉

    " makefile안에서 shell script 실행하기.

    Perhaps not the "right" way to do it like the answers already provided, but I came across this question because I wanted my makefile to run a script I wrote to generate a header file that would provide the version for a whole package of software. I have quite a bit of targets in this package, and didn't want to add a brand new prerequisite to them all. Putting this towards the beginning of my makefile worked for me

    $(shell ./genVer.sh)

    which tells make to simply run a shell command. ./genVer.sh is the path (same directory as the makefile) and name of my script to run. This runs no matter which target I specify (including clean, which is the downside, but ultimately not a huge deal to me).

    - 참고 : https://stackoverflow.com/questions/2497675/how-to-run-a-bash-script-from-a-makefile#answer-46596414

     

     

     

    - Error

    : Makefile: XXX: *** missing separator [참고1]

    " Makefile 파일은 들여쓰기를 어떤 방식으로 하느냐에 따라 영향을 받는 것으로 보인다. 예를 들어 타겟안에 작성되는 명령어들은 기본적으로 tab 을 들여쓰기로 사용해야 한다. 그런데, tab 대신에 띄어쓰기를 사용할 경우 위와 같은 에러가 발생한다. 아래 코드를 보자. 아래 Makefile 에서 2 개의 @echo 의 들여쓰기 방식이 서로 다르다. 첫 번째는 띄어쓰기를 4 번 한것이고 두 번째는 tab 을 사용해서 작성한 것이다.

    # Makefile
    CONFIG:=0
    
    $(info $(if $(CONFIG),1,0))
    
    yohda:
        @echo "yohda1"
        @echo "yohda2"

     

     

    " 특정 파일의 띄어쓰기(공백) 및 tab 을 확인하는 방법은 간단하다. vim 에서 입력 모드에서 `:set list` 를 입력하면 된다. 그렇면, 아래와 같이 띄어쓰기(공백) 은 아무 문자도 없고 tab 은 `^I` 이 있는 것을 볼 수 있다. 여기서 띄어쓰기로 표시된 첫 번째 @echo 를 tab(^I) 으로 변경하면 `*** missing separator` 에러가 해결되는 것을 볼 수 있다.

     

    '프로젝트 > 운영체제 만들기' 카테고리의 다른 글

    GDT  (1) 2023.08.05
    부트 로더  (0) 2023.08.03
    [xv6] entry  (0) 2023.08.01
    [xv6] mkfs  (0) 2023.07.30
    [xv6] Boot-loader  (0) 2023.07.29
Designed by Tistory.