Makefile 예제 및 기본

Ena JJJ·2022년 11월 30일
1

./configure
make
sudo make install

  • ./configure : 빌드 사전 작업 수행
  • make , sudo make install : 소스 디렉토리에 포함되어 있는 Makefile(또는 makefile)이라는 이름의 스크립트 파일을 읽어서 지정된 순서대로 빌드를 수행하는 방법

빌드 예제

세 개의 소스파일을 각각 컴파일하여 Object파일(*.o)을 생성하고, 이들을 한 곳에 묶는 링크 과정을 통해서 실행 파일인 app.out을 생성한다. 이때, foo와 bar에 정의된 함수를 main에서 호출하는 의존성이 존재한다.

Make를 쓰지 않고 빌드하기

다음 세 줄의 명령어를 통해 컴파일한다. 헤더파일들은 각자의 소스파일에 포함되어 있으므로 명시해주지 않아도 된다.

gcc -c -o main.o main.c
gcc -c -o foo.o foo.c
gcc -c -o bar.o bar.c

여기서 -c 옵션은 링크를 하지 않고, 컴파일만 하겠다는 의미이다. 이 옵션을 생략하면 main 함수를 찾을 수 없다는 오류가 출력된다. 그리고, 다음 명령으로 Object 파일들을 한곳에 묶는 링크 과정을 수행한다. 명령은 gcc지만, gcc 내부적으로 링커(ld)를 실행해서 실행 파일(app.out)을 생성한다.

gcc -o app.out main.o foo.o bar.o

이상 평범한 C 프로그램의 빌드 절차이다. IDE를 사용할 수 있는 프로젝트라면 빌드 버튼 원클릭으로 실행 파일을 영접할 수 있지만, 그렇지 않은 경우 이 명령들을 모두 일일이 실행해 줘야 한다. 그것도, 한번만 하면 끝나는 것이 아니라 소스코드를 수정할 때마다 매번 반복해야 하므로 불편함이 꽃피게 된다.

혹 이러한 불편함을 감수하더라도, 다음과 같은 치명적인 실수를 할 가능성도 존재한다.

gcc -o main.c main.c

이 명령의 의미는 main.c를 빌드해서 같은 이름인 main.c라는 실행파일로 저장하라는 의미이다. 즉, 이 명령을 실행하는 순간 소스파일은 사라지고 빌드된 바이너리만 남게 된다. 쉘 스크립트를 사용하면 실수를 방지한다는 측면에서는 해결책이 될 수 있지만, 이 경우 Makefile이 제공하는 강력한 기능 중 하나인 Incremental build를 사용할 수 없게 된다.
Incremental build : 반복적인 빌드 과정에서 변경된 소스코드에 의존성이 있는 대상들만 추려서 다시 빌드하는 기능. main.c 한줄만 바꾸고 다시 빌드할 때 gcc -c -o main.o main.cgcc -o app.out main.o foo.o bar.o만 수행할 수 있다.
Makefile에서는 빌드 대상 별로 의존성을 명시해주면 이에 따라 자동으로 Incremental build를 수행해주므로 매우 편리해진다.

Makefile 작성법

Bash 쉘 스크립트와 비슷하다. 빌드 대상(Target)이름으로 된 Label 별로 구분된 쉘 스크립트라고 볼 수 있다.

# 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
  gcc -c -o main.o main.c
 
 foo.o : foo.h foo.c
  gcc -c -o foo.o foo.c
  
 bar.o : bar.h bar.c
  gcc -c -o bar.o bar.c
  
 <Target> : <Dependencies>
 	<Recipe> # 반드시 Tab문자로 된 Indent가 있어야 한다.

위와 같이 Makefile을 작성하고

make

명령을 치면 한번에 app.out이 만들어진다. make 뒤에 target을 명시하면 해당 target만 만들어진다.

빌드 규칙(Rule) 블록

Makefile에서 반복되는 구조인 Rule block의 구조는 다음과 같다.

<Target>: <Dependencies>
	<Recipe>
  • Target: 빌드 대상 이름. 통상 이 Rule에서 최종적으로 생성해내는 파일명을 써준다.
  • Dependencies : 빌드 대상이 의존하는 Target이나 파일 목록. 여기에 나열된 대상들을 먼저 만들고 빌드 대상을 생성한다.
  • Recipe : 빌드 대상을 생성하는 명령. 여러 줄로 작성할 수 있으며, 각 줄 시작에 반드시 Tab문자로 된 Indent가 있어야 한다.

내장 규칙 (Built-in Rule)

Make에서 자주 사용되는 빌드 규칙들은 내장을 해서 굳이 기술하지 않아도 자동으로 처리해 준다. 소스 파일(.c)을 컴파일해서 Object 파일(.o)로 만들어주는 규칙이 여기에 해당한다. 즉, Makefile에 다음 두 줄만 써도 app.out을 빌드 할 수 있다.

app.out : main.o foo.o bar.o
	gcc -o app.out main.o foo.o bar.o

하지만 이렇게만 작성하면 Incremental build를 위한 의존성 검사에서 헤더 파일의 변경을 감지하지 못하는 문제가 발생 한다. Make는 소스 파일의 마지막 변경 시점만 확인할 뿐, 소스코드를 일일이 들여다보고 어떤 헤더 파일이 포함되었는지 추적하지는 않기 때문이다.

따라서, 아쉽지만 다음과 같이 각 Target에 대한 Dependencies까지는 명시해 줘야 한다. 이렇게 하면 헤더 파일만 변경된 경우에도 의존성이 올바르게 탐지 된다. 물론, 해당 소스파일에 헤더 파일을 추가할 때마다 이 부분을 업데이트 해줘야 한다.

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

마지막 세줄에 있는 Target의 Recipe는 모두 생략되어 있지만, Make 내부 Rule에 의해 컴파일이 수행된다.

변수 사용하기

변수를 사용하면 Makefile을 보다 깔끔하고 확장성 있게 작성할 수 있다. 변수 선언 및 사용법은 Bash 쉘 스크립트에서와 같다. 변수들 중에는 Make 내부에서도 함께 사용하는 내장 변수나(CFLAGS 등), 확장성을 용이하게 해주는 자동변수(@@<등)도 존재한다.

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 : 빌드 대상(실행 파일) 이름
    그 외에 자주 사용되는 내장 변수

  • LDFLAGS : 링커 옵션

  • LDLIBS : 링크 라이브러리
    참고로, Make에서 내부적으로 정의되어 있는 변수들은 다음 명령으로 확인할 수 있다.

  • $@: 현재 Target 이름

  • $^: 현재 Target이 의존하는 대상들의 전체 목록

  • $?: 현재 Target이 의존하는 대상들 중 변경된 것들의 목록

빌드 결과물과 부산물을 삭제하는 'Clean Rule'추가하기

Makefile의 Rule에는 빌드 대상물 뿐만 아니라, 프로젝트에서 요긴하게 사용할 수 있는 매크로를 지정해서 사용할 수 있다. 이렇게 쓰는 대표적이고, 또 널리 사용하는 매크로가 clean이다.

Clean 매크로는 빌드 결과물(e.g. app.out)과 중간 부산물들(*.o)을 모두 삭제하여 '깨끗한' 상태에서 다시 빌드할 수 있는 환경을 만들어 준다. 위와 같은 과정을 Clean build라고 한다.

Makefile에 다음 빌드 규칙을 추가하면 Clean 매크로를 사용할 수 있다.

clean:
	rm -f *.o
    rm -f $(TARGET)

사용법

make clean

Makefile 기본 패턴

다음은 통상적으로 널리 사용되는 Makefile의 기본 패턴입니다.

새로운 프로젝트를 시작할 때 처음 작성하는 Makefile은 이것은 복붙해서 수정해서 사용하면 편리합니다.
소스파일이 추가될 때마다 끝부분에 각 Object 파일들의 의존성을 기술하는 Rule들만 추가해 나가면 됩니다.

CC = <컴파일러>
CFLAGS = <컴파일 옵션>
LDFLAGS = <링크 옵션>
LDLIBS =<링크 라이브러리 목록>
OBJS = <Object 파일 목록>
TARGET =<빌드 대상 이름>

all : $(TARGET)

clean : 
	rm -f *.o
	rm -f $(TARGET)

$(TARGET):$(OBJS)
$(CC) -o $@ $(OBJS)

0개의 댓글