본 문서는 '빌드를 위한 스크립트 파일인 Makefile 에 대한 정보'를 정리한 문서입니다.
CMake와 같이 Makefile을 자동으로 만들어주는 빌드 툴로 인해 Makefile 자체를 직접 다룰 일이 줄어들었으나, 기본적인 골격은 매우 단순하므로 한 번 익혀두는 것을 권장합니다.
1. Makefile이란?
원래 리눅스 상에서 프로그램을 컴파일 할 때, 쉘상에서 컴파일을 하려면 어떤 파일들을 컴파일 하고, 어떠한 방식으로 컴파일 할 지 직접 컴파일러에게 알려줘야 합니다.
이 문제를 해결하기 위해서 리눅스에서는 make 라는 프로그램을 제공하는데, 이 프로그램은 Makefile 라는 파일을 읽어서 주어진 방식대로 명령어를 처리하게 합니다.
덕분에 많은 수의 파일들을 명령어 한 번으로 컴파일 할 수 있습니다.
컴파일 하고 싶은 makefile 위치에서 make 를 터미널 상에서 실행하게 된다면, 해당 위치에 있는 Makefile 을 찾아서 읽어 컴파일 할 수 있습니다.
2. Makefile 없이 빌드하기
2.1 Makefile 없이 빌드하기
Makefile이 없이 빌드하기 위해서는
- 각각의 소스파일를 컴파일(option: -c)하여 Object 파일(*.o)을 생성하고
- 이들을 묶는 링크 과정(Linking)을 통해 실행파일(*.out)을 만들어야 합니다.
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
위와 같은 과정으로 빌드를 한다면 실행파일을 만들 수 있습니다.
그렇다면, 이를 쉘스크립트로 만들면 되지 않는가? 그에 대한 답은 좋지 않다.
Makefile을 사용하지 않으면, Makefile만이 제공하는 기능 중 하나인 *Incremental build를 사용할 수 없게 됩니다.
- Incremental build
- 반복적인 빌드 과정에서 변경된 소스코드에 의존성(Dependency)이 있는 대상들만 추려서 다시 빌드하는 기능이다.
- 예를 들어 위의 빌드 예제에서 main.c의 한 줄만 바꾸고 다시 빌드를 한다면, main.o 컴파일(gcc -c -o main.o main.c)과 app.out링크(gcc -o app.out main.o foo.o bar.o)만 수행합니다.
2.2 Makefile로 빌드 함에 장점
Makefile에서 빌드 대상(Target)별로 의존성을 명시해 주면 이에 따라 자동으로 Incremental build를 수행하므로 매우 편리합니다.
또한, 자주 사용되는 빌드 규칙은 기술하지 않아도 내부적으로 자동으로 처리해 주기 때문에 쉘 스크립트에 비해 편리하고 깔끔합니다.
3. Makefile 을 사용하여 빌드하기
3.1 Makefile 을 사용하여 빌드
컴파일 하고 싶은 makefile 위치에서 make 를 터미널 상에서 실행하게 된다면, 해당 위치에 있는 Makefile 을 찾아서 읽어 컴파일 할 수 있습니다.
3.2 멀티 코어를 활용해서 Make 속도를 올리자
그냥 make 를 실행하게 되면 1 개의 쓰레드만 실행되어서 속도가 느립니다.
만일 여러분의 컴퓨터가 멀티 코어 CPU 를 사용한다면, make 인자로 -j 뒤에 몇 개의 쓰레드를 사용할 지 전달하여 여러 개의 쓰레드에서 돌릴 수 있습니다.
$ make -j8
만약에 내 컴퓨터의 코어 개수를 모른다면 리눅스 터미널의 경우, -j 뒤에 $(nproc) 인자를 사용한다면 내 컴퓨터의 현재 코어 개수를 사용하여 실행할 수 있습니다
$ make -j$(nproc)
주의!!!!
프로젝트를 처음 빌드할 경우 멀티쓰레드를 사용하여 빌드할 경우 오류가 발생할 수 있습니다.
Makefile 특성상 정해진 순서를 가지고 빌드를 하게 되는데 멀티 쓰레드를 사용하면 순서가 달라질 수 있습니다.
4. Makefile 을 만들자!!
4.1 빌드 규칙 블록
Makefile에서 반복되는 구조인 Rule block의 구조는 다음과 같습니다.
<Target>: <Dependencies>
<Recipe>
위의 명칭은 GNU make 공식 매뉴얼에서 그대로 들고 온 것인데, 쉽게 설명해서 다음과 같은 의미입니다.
- Target: 빌드 대상 이름.
- 통상 이 Rule에서 최종적으로 생성해내는 파일명을 씁니다.
- Dependencies: 빌드 대상이 의존하는 Target이나 파일 목록.
- 여기에 나열된 대상들을 먼저 만들고 빌드 대상을 생성합니다.
- Recipe: 빌드 대상을 생성하는 명령.
- 여러 줄로 작성할 수 있으며, 각 줄 시작에 반드시 Tab문자로 된 Indent가 있어야 합니다.
4.2 Makefile Built-in Rule:
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
자주 사용되는 빌드 규칙들은 내장을 해서 굳이 기술하지 않아도 자동으로 처리합니다.
- 예를 들어, 소스 파일(*.c)을 컴파일해서 Object 파일(*.o)로 만들어 주는 규칙을 포함합니다.
하지만, 각 Target에 대한 Dependencies까지는 명시해 줘야 합니다.
4.3 변수 사용하기
CC=gcc
CFLAGS=-g -Wall
OBJS=main.o foo.o bar.o
TARGET=app.out
$(TARGET): $(OBJS)
$(CC) -o $@ $(OBJS)
main.o: foo.h bar.h main.c
foo.o: foo.h foo.c
bar.o: bar.h bar.c
정의한 각 변수의 의미는 다음과 같다.
- CC: 컴파일러
- CFLAGS: 컴파일 옵션
- OBJS: 중간 산물 Object 파일 목록
- TARGET: 빌드 대상(실행 파일) 이름
Makefile 내 자동 정의되는 변수
- $@: 현재 Target 이름
- $^: 현재 Target이 의존하는 대상들의 전체 목록
- $?: 현재 Target이 의존하는 대상들 중 변경된 것들의 목록
- 참고: http://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html
4.4 변수 할당( = 과 := 차이 )
= 를 사용해서 변수를 정의하였을 때, 정의에 다른 변수가 포함되어 있다면 해당 변수가 정의되기 될 때 까지 변수의 값이 정해지지 않습니다.
- B = $(A)
A = a - 상기의 예시의 경우, A 값이 정해지고 B 값이 정해지기에 B = a가 됩니다.
:= 로 변수를 정의할 경우, 해당 시점에의 변수의 값만 확인 합니다.
- B := $(A)
A := a - 상기의 예시의 경우, B 값이 해당 시점을 기준으로 정해지기에 B 에 빈 문자열이 들어갑니다.
- Tip!! A = 와 같이 자기 자신을 수정하고 싶다면 := 를 사용해야지 무한 루프를 피할 수 있습니다.
Tip!! clean Rule 추가하기
Clean 매크로는 빌드 결과물(e.g. app.out)과 중간 부산물들(*.o )을 모두 삭제하여 '깨끗한' 상태에서 다시 빌드할 수 있는 환경을 생성합니다.
Makefile에는 다음과 같이 추가하며, cmd에 make clean 명령으로 사용할 수 있습니다.
clean:
rm -f *.o
rm -f $(TARGET)
5. CMake: Makefile을 쉽고 간편하게 만들고 관리하기
프로젝트의 규모가 커지고 복잡해질수록 Makefile을 유지/보수하기 힘듭니다. 대규모 프로젝트에서는 빌드 대상 소스 파일들을 관리하는 것부터 시작해서, 빌드 전/중/후 수행하는 작업과 패키지를 구성하는 부속품들을 생성해 내는 작업 등등 여러 가지 Build Step들이 복잡하게 뒤엉키게 됩니다.
- 프로그램의 버전을 명시하는 등의 이유로 빌드에 전 특정 헤더 파일들을 자동 생성하는 경우
- 실행 파일 외에 공유 라이브러리들을 함께 생성하는 등 빌드 대상물이 한두개가 아닌 경우
- 프로젝트에 포함된 서브모듈(써드파티 프로그램들)들이 다단계로 존재하는 경우
- 빌드 전 프로그램에 사용되는 리소스 파일들은 한 데 묶어서 가상 파일시스템을 만들어야 하는 경우
- 빌드 완료된 실행 파일로부터 임베디드 프로세서에 퓨징하기 위한 바이너리를 생성해야 하는 경우
따라서 CMake로 Build Step을 기술하여 Makefile을 생성하면, 프로젝트를 관리하기 쉬워집니다!
추가로, 이 외에도 많은 이점을 가지고 있습니다.
Make가 유닉스 계열만 지원하는 것과 달리, CMake는 유닉스 계열인 리눅스, BSD, OS X뿐만 아니라 윈도우즈 계열도 지원하며, CMakeLists.txt은 Visual Studio, Eclipse, Qt Creator 등 다양한 IDE에서 기본으로 지원하여 쉽게 사용가능합니다.
마무리
본 문서에서는 빌드를 위한 스크립트 파일인 Makefile 에 대해 다루었으며, 다음 글에서는 CMake에 대한 간단한 설명과 참고하면 좋은 자료들을 소개합니다. 앞으로 빌드 환경을 구축하기 위한 CMakefile을 구성하는 방법도 차차 소개할 것입니다.
[CMake] CMake 소개 및 필요성: Modern CMake 3.23+
참고
- [씹어먹는 C++] Make 사용 가이드 (Makefile 만들기): https://modoocode.com/311
- [Make 튜토리얼] Makefile 예제와 작성 방법 및 기본 패턴: https://www.tuwlab.com/ece/27193
'Study: DeveloperTools(DevTool) > DevTool: CMake' 카테고리의 다른 글
[CMake] FetchContent: 외부 라이브러리를 사용하자~ (0) | 2022.06.24 |
---|---|
[CMake] Effective Modern CMake 정리 (0) | 2022.06.24 |
[CMake] An Introduction to Modern CMake 정리 (0) | 2022.06.24 |
[CMake] CMake 소개 및 필요성: Modern CMake 3.23+ (0) | 2022.06.24 |
[C++] C++ 빌드 시스템(툴)에는 뭐가 있을까? (0) | 2022.06.24 |