================================================
make ( makefile ), cmake ( CMakeLists.txt ) 에 대하여
1. make (makefile) 기초 - 본글 https://bahk33.tistory.com/193
2. cmake ( CMakeLists.txt ) 기초, 간단 예제 https://bahk33.tistory.com/195
================================================
================================================
가. 일반사항
컴퓨터에서 소스를 만든뒤 타겟보드에서 실행되는 파일로 만들기 위한 작업을 간단히 말하여 빌드라 합니다
빌드는 컴파일과 링크 2단계 입니다.
컴파일 = .c 파일을 .o 로 만드는일 ( 소스를 어셈블리어로 바꾸는거 )
링크 = .o 를 합쳐서 1개 파일로 만드는 일,
빌드 = 컴파일 + 링크
컴파일과 링크는 컴파일러(cc, gcc, g++ 등) 라는 실행 파일이 합니다.
make(.exe) 라는 실행 파일은 이러한 작업을 묶어서 할 수 있게 합니다.
makefile 라는 파일은 make 가 읽어서 그 내용에 따라 작업을 할 수 있게 작업 절차를 넣어 놓은 것 입니다.
1. Makefile 없이 (make 실행 없이) 빌드 작업
> 컴파일 작업과 링크 작업을 하나씩 해야 합니다.
gcc -c -o main.o main.c
gcc -c -o foo.o foo.c
gcc -c -o bar.o bar.c
gcc -o app.out main.o foo.o bar.o
> 이상과 같은 작업을 수동으로 한다면, 소스를 수정 할 때마다 하는게 번거럽지요. 그럼 스크립트(.bat 등)에 넣어서
작업 할 수도 있지만, Incremental build 등 편리 하게 하기 위한것이 make 를 이용 하는 것 입니다.
> Incremental build
- 수정 된 파일 만 처리(컴파일 등)
- 위에서 foo.c 만 수정 되었다면,
gcc -c -o foo.o foo.c
gcc -o app.out main.o foo.o bar.o
와 같이 바뀐 부분만 처리 하는 것
> 다음은 make 를 실행 하기 위하여 Makefile 만들어 봅니다.
나. Makefile 만들기
1. Makefile 규칙
> 목적물을 만들기 위하여 준비 할것들은 무엇이고, 어찌 어찌 하면 된다 라는 뜻 입니다.
=========================
<Target 목적물> : <Dependencies 필요한것들>
<Recipe 처리방법>
==========================
> 위 보기 들엇던것을 makefile 로 만들면 다음과 같읍니다.
app.out: main.o foo.o bar.o
gcc -o app.out main.o foo.o bar.o
main.o: foo.h bar.h main.c
foo.o: foo.h foo.c
bar.o: bar.h bar.c
2. 변수 사용 하기
- 변수를 사용 하여 간단히 합니다.
# 변수 설정 부분
CC=gcc
CFLAGS= -Wall -O2
OBJS=main.o foo.o bar.o
TARGET=app.out
# 목적물 "app.out" 을 만들기 위한 링크 작업
$(TARGET): $(OBJS)
$(CC) $(OBJS) $(CXXFLAGS) -o $@
# 각 소스들을 컴파일 하여 .o 로 만드는 작업
foo.o : foo.h foo.c
$(CC) $(CXXFLAGS) -c foo.c
bar.o : bar.h bar.c
$(CC) $(CXXFLAGS) -c bar.c
main.o : main.c foo.h bar.h
$(CC) $(CXXFLAGS) -c main.c
> make 를 실행 해 보면
$ make main
gcc -Wall -O2 -c foo.c
gcc -Wall -O2 -c bar.c
gcc -Wall -O2 -c main.c
gcc -Wall -O2 foo.o bar.o main.o -o main
와 같이 실행 됩니다.
3. 패턴 사용 하기
> 보기의 경우 파일이 3 개 밖에 없어서 다행이였지만 실제 프로젝트에는 수십~ 수백 개의 파일들을 다루게 될 것입니다. 그런데, 각각의 파일들에 대해서 모두 빌드 방식을 명시해준다면 Makefile 의 크기가 엄청 커지겠지요.
다행이도 Makefile 에서는 패턴 매칭을 통해서 특정 조건에 부합하는 파일들에 대해서 간단하게 recipe (처리방안) 를 작성할 수 있게 해줍니다.
foo.o : foo.h foo.c
$(CC) $(CXXFLAGS) -c foo.c
bar.o : bar.h bar.c
$(CC) $(CXXFLAGS) -c bar.c
에서, 일단 먼저 비슷하게 생긴 위 두 명령들을 어떻게 하면 하나로 간단하게 나타내 보겠습니다.
%.o: %.c %.h
$(CC) $(CXXFLAGS) -c $<
> 먼저 %.o 는 와일드카드로 따지면 마치 *.o 와 같다고 볼 수 있습니다.
즉, .o 로 끝나는 파일 이름들이 타겟이 될 수 있겠지요.
예를 들어서 foo.o 가 타겟이라면 % 에는 foo 가 들어갈 것이고 bar.o 의 경우 % 에는 bar 가 들어갈 것입니다.
> 단지,
* 패턴은 Target 과 Dependencies 부분에만 사용할 수 있습니다.
* Recipe 부분에서는 패턴을 사용할 수 없습니다.
따라서 컴파일러에 foo.cc 를 전달하기 위해서는 Makefile 의 자동 변수를 사용해야 합니다.
$< 의 경우 Dependencies 에서 첫 번째 파일의 이름에 대응되어 있는 자동 변수 입니다. 위 경우 foo.cc 가 되겠지요. 따라서 위 명령어는 결과적으로
foo.o: foo.c foo.h
$(CC) $(CXXFLAGS) -c foo.c
가 되어서 이전의 명령어와 동일하게 만들어냅니다.
>. 이미 설정된 자동 변수 들
- $@: 현재 Target 이름( Target )
- $< : 의존 파일( Dependencies ) 목록에 첫 번째 파일에 대응됩니다.
- $^ : 의존하는 대상들의 전체 목록
- $? : 의존하는 대상들 중 변경된 것들의 목록
- $+ : $^ 와 비슷하지만, 중복된 파일 이름들 까지 모두 포함합니다.
- 더 자세한 것은 ( https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html ) 를 보셔요
> 하지만 애석하게도 위 패턴으로는
main.o : main.c foo.h bar.h
$(CC) $(CXXFLAGS) -c main.c
를 표현하기에는 부족합니다.
왜냐하면 의존 파일 목록에 main.h 가 없고 foo.h 와 bar.h 가 있기 때문이죠. 사실 곰곰히 생각해보면 이 의존파일 목록에는 는 해당 소스 파일이 어떠한 헤더파일을 포함하냐에 결정되어 있습니다.
main.c 가 foo.h 와 bar.h 를 include 하고 있기 때문에 main.o 의 Dependencies 로 main.c 외에도 foo.h 와 bar.h 가 들어가 있는 것입니다.
물론 매번 이렇게 일일히 추가할 수 있겠지만, 소스 파일에 헤더 파일을 추가할 때 마다 Makefile 을 바꿀 수는 없는 노릇이니까요. 하지만 다행이도 컴파일러의 도움을 받아서 의존파일 목록 부분을 작성할 수 있습니다.
4. 자동으로 Dependencies 만들기
> 컴파일 시에 -MD 옵션을 추가해서 컴파일 해봅시다.
$ gcc -c -MD main.c
이 명령줄로 main.d 라는 파일이 만들어 집니다. 파일 내용을 살펴보면;
$ type main.d
main.o: main.cc /usr/include/stdc-predef.h foo.h bar.h
> 놀랍게도 마치 Makefile 의 Target : Dependencies 인것 같은 부분을 생성하였습니다. 네. 컴파일 시에 -MD 옵션을 추가해주면, 목적 파일 말고도 컴파일 한 소스파일을 타겟으로 하는 의존파일 목록을 담은 파일을 생성해줍니다.
> 참고로 main.c, foo.h, bar.h 까지는 이해가 가는데 왜 생뚱맞은 /usr/include/stdc-predef.h 이 들어가 있냐고 물을 수 있는데, 이 파일은 컴파일러가 컴파일 할 때 암묵적으로 참조하는 헤더 파일이라고 보시면 됩니다. 아무튼 이 때문에 컴파일러가 생성한 의존 파일 목록에는 포함되었습니다.
> 문제는 이렇게 생성된 main.d 를 어떻게 우리의 Makefile 에 포함할 수 있냐 입니다. 이는 생각보다 간단합니다.
CC = gcc
CXXFLAGS = -Wall -O2
OBJS = foo.o bar.o main.o
%.o: %.c %.h
$(CC) $(CXXFLAGS) -c $<
main : $(OBJS)
$(CC) $(CXXFLAGS) $(OBJS) -o main
.PHONY: clean
clean:
rm -f $(OBJS) main
include main.d
위 include main.d 는 main.d 라는 파일의 내용을 Makefile 에 포함하라는 의미 입니다.
그렇다면 차라리
%.o: %.c %.h
$(CC) $(CXXFLAGS) -c $<
부분을 아예 컴파일러가 생성한 .d 파일로 대체할 수는 없을까요? 물론 있습니다!
CC = gcc
CXXFLAGS = -Wall -O2
OBJS = foo.o bar.o main.o
%.o: %.c
$(CC) $(CXXFLAGS) -c $<
main : $(OBJS)
$(CC) $(CXXFLAGS) $(OBJS) -o main
.PHONY: clean
clean:
rm -f $(OBJS) main
-include $(OBJS:.o=.d)
> $(OBJS:.o=.d) 부분은 OBJS 에서 .o 로 끝나는 부분을 .d 로 모두 대체하라는 의미 입니다.
즉, 해당 부분은 -include foo.d bar.d main.d 가 되겠죠. 참고로 foo.d 나 bar.d 가 include 될 때 이미 있는 %.o: %.cc 는 어떻게 되냐고 물을 수 있는데 같은 타겟에 대해서 여러 의존 파일 목록들이 정해져 있다면 이는 make 에 의해 모두 하나로 합쳐집니다. 따라서 크게 걱정하실 필요는 없습니다.
> 덧붙여 include 에서 -include 로 바꾸었는데, -include 의 경우 포함하고자 하는 파일이 존재하지 않아도 make 메세지를 출력하지 않습니다.
> 맨 처음에 make 를 할 때에는 .d 파일들이 제대로 생성되지 않은 상태이기 때문에 include 가 아무런 .d 파일들을 포함하지 않습니다. 물론 크게 문제 없는 것이 어차피 .o 파일들도 make 가 %.o: %.cc 부분의 명령어들을 실행하면서 컴파일을 하기 때문에 다음에 make 를 하게 될 때에는 제대로 .d 파일들을 로드할 수 있겠죠.
5. 변수를 정의 하는 2가지 방법
Makefile 상에서 변수를 정의하는 방법으로
> "=" 를 사용해서 정의하는 방법과
> ":=" 를 사용해서 정의하는 방법이 있습니다.
이 둘은 살짝 다릅니다.
> "=" 를 사용해서 변수를 정의하였을 때, 정의에 다른 변수가 포함되어 있다면 해당 변수가 정의되기 될 때 까지 변수의 값이 정해지지 않고 대기 하고 있다가, 해당 변수가 정의 되면 변수 값이 정해 집니다..
예를 들어서
B = $(A)
C = $(B)
A = a
여기서 B 는 A 의 값을 참조하고, C 는 B 의 값을 참조하고 있습니다. 하지만 B = 를 실행한 시점에서 A 가 정의되지 않았으므로 B 는 그냥 대기하고 있읍니다. 즉 A 가 실제로 정의되지 않았기 때문에 B 와 C 가 결정되지 않습니다. 이후 A = a 를 통해 A 가 a 로 정의 되면, 그 값을 보고 C 가 a 로 결정됩니다.
> ":=" 로 변수를 정의할 경우, 해당 시점에의 변수의 값만 확인 합니다.
B := $(A)
A = a
위 경우 B 는 그때 당시 A가 설정 되지 않았지만, 설정 됩니다. 즉 A가 설정 된것이 없으므로 B는 그냥 빈 문자열이 됩니다.
> 대부분의 상황에서는 = 나 := 중 아무거나 사용해도 상관 없습니다. 하지만 만일 변수들의 정의 순서에 크게 구애받고 싶지 않다면 = 를 사용하는 것이 편합니다.
단지, A = 와 같이 자기 자신을 수정하고 싶다면 := 를 사용해야지 무한 루프를 피할 수 있습니다.
만 명심하면 됩니다.비슷하지만, 중복된 파일 이름들 까지 모두 포함합니다.
6. PHONY
- 빌드가 완료 된 뒤 ~.o 와 같은 파일들은 필요 하지 않읍니다. 이들을 지우는 것도 Makefile 에 넣어서 사용 합니다.
clean:
rm -f $(OBJS) main
이것을 실행 ( make clean ) 하면, 생성된 모든 목적 파일과 main 을 지워버림을 알 수 있습니다.
그런데, 만약에 실제로 clean 이라는 파일이 디렉토리에 존재 하고 있다면 어떨까요? 우리가 make clean 을 하게 되면, clean 파일이 있으니까, 처리 방안(recipe) 를 실행 안해도 되겠네! 하면서 그냥 make clean 명령을 무시해버리게 됩니다.
$ dir clean
clean
$ make clean
make: 'clean' is up to date.
실제로 디렉토리에 clean 이라는 파일을 만들어놓고 실행해보면 위와 같이 이미 clean 은 최신이라며 recipe 실행을 거부합니다.
이와 같은 상황을 막기 위해서는 clean 을 PHONY 라고 등록하면 됩니다.
* Phony 는 '가짜의, 허위의' 이라는 뜻입니다
.PHONY: clean
clean:
rm -f $(OBJS) main
이제 make clean 을 하게 되면 clean 파일의 유무와 상관 없이 언제나 해당 타겟의 명령을 실행하게 됩니다.
7. 추가 사항
기본적인것 하였는데, 조금 더 깊이 들어 가면,
먼저 간단한 구조를 생각 합니다.
$ tree
.
├── include
│ ├── bar.h
│ └── foo.h
├── Makefile
├── obj
└── src
├── bar.c
├── foo.c
└── main.c
> 이와 같은 경우 Makefile 은 다음과 같읍니다.
CC = g++
# C++ 컴파일러 옵션
CXXFLAGS = -Wall -O2
# 링커 옵션
LDFLAGS =
# 헤더파일 경로
INCLUDE = -Iinclude/
# 소스 파일 디렉토리
SRC_DIR = ./src
# 오브젝트 파일 디렉토리
OBJ_DIR = ./obj
# 생성하고자 하는 실행 파일 이름
TARGET = MyPrj1
# Make 할 소스 파일들
# wildcard 로 SRC_DIR 에서 *.c 로 된 파일들 목록을 뽑아낸 뒤에
# notdir 로 파일 이름만 뽑아낸다.
# (e.g SRCS 는 foo.c bar.cc main.c 가 된다.)
SRCS = $(notdir $(wildcard $(SRC_DIR)/*.c))
OBJS = $(SRCS:.c=.o)
DEPS = $(SRCS:.c=.d)
# OBJS 안의 object 파일들 이름 앞에 $(OBJ_DIR)/ 을 붙인다.
OBJECTS = $(patsubst %.o,$(OBJ_DIR)/%.o,$(OBJS))
DEPS = $(OBJECTS:.o=.d)
all: main
$(OBJ_DIR)/%.o : $(SRC_DIR)/%.c
$(CC) $(CXXFLAGS) $(INCLUDE) -c $< -o $@ -MD $(LDFLAGS)
$(TARGET) : $(OBJECTS)
$(CC) $(CXXFLAGS) $(OBJECTS) -o $(TARGET) $(LDFLAGS)
.PHONY: clean all
clean:
rm -f $(OBJECTS) $(DEPS) $(TARGET)
-include $(DEPS)
> SRCS = $(notdir $(wildcard $(SRC_DIR)/*.c))
- 먼저 SRC_DIR 안에 있는 모든 파일들을 SRCS 로 읽어들이려 하고 있습니다.
- wildcard 는 함수로 해당 조건에 맞는 파일들을 뽑아내게 되는데,
3개 파일 foo.c, bar.c, main.c 가 있으므로 $(wildcard $(SRC_DIR)/*.c) 의 실행 결과는
./src/foo.c ./src/bar.c ./src/main.c 가 될 것입니다.
- 이후 foo.c bar.c main.c 와 같이 경로를 제외한 파일 이름만 뽑아내기 위해 notdir 함수를 사용합니다.
notdir 은 앞에 오는 경로를 날려버리고 파일 이름만 깔끔하게 추출해줍니다.
> OBJS = $(SRCS:.cc=.o)
이 부분에서 OBJS 는 foo.o bar.o main.o 가 될 것입니다.
> 이제 이 OBJS 를 바탕으로 실제 .o 파일들의 경로를 만들어내고 싶습니다.
이를 위해서는 이들 파일 이름 앞에 $(OBJ_DIR)/ 을 붙여줘야 겠지요. 이를 위해선 patsubst 함수를 사용하면 됩니다.
> OBJECTS = $(patsubst %.o,$(OBJ_DIR)/%.o,$(OBJS))
patsubst 함수는 $(patsubst 패턴,치환 후 형태,변수) 의 같은 꼴로 사용합니다.
따라서 위 경우 $(OBJS) 안에 있는 모든 %.o 패턴을 $(OBJ_DIR)/%.o 로 치환해라 라는 의미가 될 것입니다.
아무튼 덕분에 OBJECTS 에는 이제 ./obj/foo.o ./obj/bar.o ./obj/main.o 가 들어가게 됩니다.
그 뒤에 내용은 앞의 글을 잘 따라 오신 분들이라면 잘 이해 하실 수 있으리라 믿습니다.
> 헤더 파일
컴파일러 옵션에 -Iinclude/ 를 추가해주면 됩니다. 여기서 include 는 헤더파일 경로 입니다.
8. 멀티코어 처리
요즘 CPU 는 멀티코어가 많으니, make 할때 쓰레드를 코어 수에 맞게 한다면 그만큼 빨리 컴파일이 됩니다.
이 옵션은 "-j" 로 그 뒤에 숫자를 적으면 됩니다. 내가 사용 하는 PC 의 코어 개수가 8이면
> make -j8
이라 하면 되고 혹시 코어 수를 모른 다면,
> make -j$(nporc)
라 하면 자동으로 내 PC의 코어수 로 바뀌어 처리 됩니다.
'개발 > embed' 카테고리의 다른 글
cmake ( CMakeLists.txt ) 기초, 간단 예제 (0) | 2025.02.07 |
---|---|
CMakeCache.txt: Error: unrecognized option '--major-image-version' (0) | 2025.01.21 |
gcc 로 nuvoton source 직접 build(compile) 하기 (0) | 2025.01.20 |
Nuvoton 제공 Sample Code를 Keil 에서 Compile 하기 (0) | 2025.01.10 |
win11에 JTAGICE mkII driver설치 ( ATMega AVR Studio 4.19 ) (0) | 2024.12.10 |