본문 바로가기

[자동화] make로 C 프로그램 빌드와 테스트 자동화하기

C 소스 코드를 실행 가능한 파일로 컴파일하기 위해 명령어를 직접 입력하려면 꽤나 수고스럽다. 예를 들어, program.c라는 소스 코드를 빌드하려면 아래와 같은 명령어를 매번 입력해줘야 한다.

gcc -Wall -O2 -I./include -o basic/build/program basic/program.c lib/*.c

 

make 라는 도구를 사용하면 빌드, 테스트 등을 자동화할 수 있다. make의 설정은 Makefile에 작성한다.

 

Makefile 예시

make라는 도구와 Makefile을 처음 접하게 된 것은 카네기 멜론 대학교의 CS:APP Lab 과제를 수행하면서이다. CS:APP Lab 과제물은 학생들이 자신의 코드를 테스트해볼 수 있도록 테스트 케이스와 Makefile을 제공한다. 다음은 Unix Shell Lab 이라는 Lab의 과제물에 있는 Makefile이다.

(출처: https://csapp.cs.cmu.edu/3e/labs.html)

# Makefile for the CS:APP Shell Lab

TEAM = NOBODY
VERSION = 1
HANDINDIR = /afs/cs/academic/class/15213-f02/L5/handin
DRIVER = ./sdriver.pl
TSH = ./tsh
TSHREF = ./tshref
TSHARGS = "-p"
CC = gcc
CFLAGS = -Wall -O2
FILES = $(TSH) ./myspin ./mysplit ./mystop ./myint

all: $(FILES)

##################
# Handin your work
##################
handin:
	cp tsh.c $(HANDINDIR)/$(TEAM)-$(VERSION)-tsh.c


##################
# Regression tests
##################

# Run tests using the student's shell program
test01:
	$(DRIVER) -t trace01.txt -s $(TSH) -a $(TSHARGS)
test02:
	$(DRIVER) -t trace02.txt -s $(TSH) -a $(TSHARGS)
test03:
	$(DRIVER) -t trace03.txt -s $(TSH) -a $(TSHARGS)
# 이하 중략

# Run the tests using the reference shell program
rtest01:
	$(DRIVER) -t trace01.txt -s $(TSHREF) -a $(TSHARGS)
rtest02:
	$(DRIVER) -t trace02.txt -s $(TSHREF) -a $(TSHARGS)
rtest03:
	$(DRIVER) -t trace03.txt -s $(TSHREF) -a $(TSHARGS)
# 이하 중략

# clean up
clean:
	rm -f $(FILES) *.o *~

 

Makefile 작성 방법

위 예시를 기반으로 Makefile 작성 방법을 정리하면 다음과 같다.

 

1. Makefile 내부에서 사용할 변수 정의

Makefile 내부에서 사용할 변수를 다음과 같이 정의할 수 있다.

테스트 파일 경로, 빌드 파일 경로, 컴파일 도구, 컴파일 옵션, 인자 등을 변수로 지정하면 자동화할 명령어를 작성할 때 사용할 수 있어 편리하다.

DRIVER = ./sdriver.pl
TSH = ./tsh
TSHREF = ./tshref
PROGRAMS = ./myspin ./mysplit ./mystop ./myint
TSHARGS = "-p"
CC = gcc
CFLAGS = -Wall -O2
MSG = "*INFO: PID and file name can be different."

 

2. 타켓에 Shell 명령어 할당

Makefile에 다음과 같이 작성하면 커맨드 창에 make test01 이렇게 입력했을 때 $(DRIVER) -t trace01.txt -s $(TSH) -a $(TSHARGS)가 수행된다. 이때 $(변수명)은 1번에서 정의한 변수들이다.

test01:
	$(DRIVER) -t trace01.txt -s $(TSH) -a $(TSHARGS)

 

test01을 타겟이라고 부른다. 타겟 뒤에 콜론 (:)을 입력한 다음에 해당 타겟에 할당할 Shell 명령어를 입력한다.

 

3. 타겟에 의존성 추가

만약 어떤 타겟을 만들기 전에 먼저 수행해야 할 작업이 있다면 다음과 같이 타겟에 의존성을 추가할 수 있다.

test01: $(TSH)
	$(DRIVER) -t trace01.txt -s $(TSH) -a $(TSHARGS)

 

이렇게 타겟 test01에 $(TSH)라는 의존성을 추가하면, 커맨드 창에 make test01 에 입력했을 때 타겟에 할당된 Shell 명령어를 실행시키기 전에 $(TSH)에 해당하는 작업이 먼저 진행된다. 즉, 해당 타겟의 명령어가 ./tsh 가 있어야 실행시킬 수 있는 명령어라면, ./tsh 를 먼저 빌드한 후에 해당 명령어를 실행시키는 것이다.

 

Makefile의 암묵적 규칙

Makefile에는 몇 가지 암묵적 규칙이 있다.

 

(1) make만 입력하면 Makefile의 첫 번째 타겟이 실행된다.

커맨드 창에 타겟 없이 make 이렇게만 입력해도 어떤 작업이 수행된다. 이는 make를 입력했을 때 암묵적으로 첫 번째로 정의된 타겟이 실행되기 때문이다. 위 예시 Makefile의 경우 all 이라는 타겟이 가장 처음에 정의되어 있기 때문에 커맨드 창에 make를 입력하면 make all을 수행한 것과 같은 결과가 나온다.

 

(2) 변수를 활용하여 컴파일 명령어가 자동 완성된다.

타겟에 gcc 컴파일 명령어를 할당하지도 않았는데 타겟을 실행시키면 gcc 컴파일이 정상적으로 잘 이루어지는 경우가 있다.

TSH = ./tsh
CC = gcc
CFLAGS = -Wall -O2
FILES = $(TSH) ./myspin ./mysplit ./mystop ./myint

all: $(FILES)

 

예를 들어 여기서 all 이라는 타겟에는 빌드 파일의 경로만 전달하고 있는데도 make all을 입력하면 tsh.c, myspin.c, mysplit.c, mystop.c, myint.c를 컴파일하는 명령어가 잘 수행된다. 이는 Makefile이 파일에 정의되어 있는 변수 CC, CLFAGS, LDFLAGS를 활용하여 자동으로 컴파일 명령어를 완성시켜주기 때문이다.

 

Makefile 테스트 추가

Unix Shell Lab 과제를 수행하면서 테스트 자동화를 위해 타겟을 추가했다.

 

(1) 테스트 결과를 터미널이 아닌 파일에 출력

Makefile에는 기본으로 타겟 test01 이라는 타겟이 제공되어 있어서 커맨드 창에 make test01을 입력하면 내가 구현한 ./tsh (tiny shell)에서 trace01.txt 라는 테스트 케이스를 실행시키고 터미널에 output을 출력한다. 이를 파일에 저장하는 타겟 test01.result를 추가하고 여기에 make test01 > test01.result 라는 명령어를 할당하여 테스트의 결과가 터미널이 아닌 파일에 출력되도록 했다.

test01.result:
	make test01 > ./test01.result

 

(2) 테스트의 출력 결과와 레퍼런스의 출력 결과 비교

Unix Shell Lab 과제물에는 레퍼런스 Shell의 실행 파일인 ./tshref(모범 답에 해당)이 있다. Makefile에는 기본으로 rtest01이라는 타겟이 제공되어 있어서 커맨드 라인에 make rtest01 을 입력하면 ./tshref (reference tiny shell)에서 trace01.txt 라는 테스트 코드를 실행시키고 터미널에 output을 출력한다. 이 output과 (1)에서 산출한 output을 비교하는 방식으로 본인의 코드를 점검하게 된다.

하지만 이는 불편하기 때문에 이 레퍼런스 Shell의 출력 결과도 (1)번처럼 파일에 저장되게 만드는 타겟 rtest02.result 를 추가해줬고, test01.result와 rtest01.result를 diff check 하는 타겟 test01.diff을 추가해줬다. 이때 타겟 test01.diff 에는 의존성으로 타겟 test01.result와 rtest01.result을 지정해줬다.

rtest01.result:
	make rtest01 > ./rtest01.result
    
test01.diff: test01.result rtest01.result
	diff -u ./rtest01.result ./test01.result

 

(3) 모든 테스트의 결과를 하나의 파일에 저장하고 출력 결과 비교

Unix Shell Lab 과제물에는 테스트 케이스가 총 16개가 있다. 이 모든 케이스에 대해서 일일이 make를 수행하는 것도 번거롭기 때문에 ./tsh와 ./tshref 각각에서 모든 테스트를 돌린 결과를 각각 하나의 파일에 저장하는 타겟 tests.result와 rtests.result 를 추가해줬고, 이 두 파일을 diff check하는 타겟 tests.diff 를 추가해줬다. 이때도 마찬가지로 tests.diff 에는 의존성으로 tests.resut와 rtests.result를 추가해줬다.

tests.result:
	$(DRIVER) -t trace01.txt -s $(TSH) -a $(TSHARGS) > ./tests.result
	$(DRIVER) -t trace02.txt -s $(TSH) -a $(TSHARGS) >> ./tests.result    
# (중략)

rtests.result:
	$(DRIVER) -t trace01.txt -s $(TSHREF) -a $(TSHARGS) > ./rtests.result
	$(DRIVER) -t trace02.txt -s $(TSHREF) -a $(TSHARGS) >> ./rtests.result
# (중략)

tests.diff: tests.result rtests.result
	diff -u ./rtests.result ./tests.result > ./tests.diff

 

다음은 업데이트한 Makefile이 있는 리포지토리이다.

https://github.com/jee-in/shell-lab

 

GitHub - jee-in/shell-lab: [CS:APP] Unix Shell Lab

[CS:APP] Unix Shell Lab. Contribute to jee-in/shell-lab development by creating an account on GitHub.

github.com