[make] # 2. 매크로

문연수·2023년 2월 12일
0

make

목록 보기
3/6
post-thumbnail

 책에서 등장한 명령 행 이란 단어는 문맥에 맞게 커맨드 라인 혹은 명령 행 이라고 옮겼다. 전부 다 명령 행이라고 하니까 기술 파일의 명령 행인지 프롬프트에서의 커맨드 라인인지 도통 알 수가 없다.


 타깃들이 수십 개의 파일들에 종속되거나 여러 개의 서로 다른 버전으로 만들어지기 마련인 실제 프로젝트의 기술 파일에서 반복되는 텍스트의 양을 실로 엄청나리라 생각할 수 있으나 실제 사용되는 기술 파일을 열어보면 뜻밖에도 보통 10~50 줄 정도로 간결하다. 이렇게 간결하게 표현할 수 있는 것은 make 가 지닌 강력한 두 가지 기능, 매크로확장자 규칙 덕분에 가능하다.

LIBES = -lx11
objs = drawable.o plot_points.o root_data.o
CC=/usr/fred/cc
23 = "This is the (23)rd run"
OPT = 
DEBUG_FLAG = 
BINDIR = /usr/local/bin

 위와 같은 매크로를 정의한 기술 파일에 아래와 같은 내용이 들어갔다고 생각해 보자:

plot: ${objs}
	${CC} -o plot ${DEBUG_FLAG} ${objs} ${LIBES}
    mv plot ${BINDIR}

make plot 을 실행하면 명령은 다음과 같이 실행된다:

/usr/fred/cc -o plot drawable.o plot_points.o root_data.o -lx11
mv plot /usr/fred/bin

1. 구문 규칙

- 기본 규칙

 매크로 정의는 등호(=)를 포함하는 하나의 문장(행)이다. make 는 등호 왼쪽의 이름(문자와 숫자의 조합)을 등호 오른쪽의 문자열에 연관시킨다. 한 가지 주의해야 할 점은 문자열을 작은따옴표('')나 큰따옴표("")로 구분해서는 안 된다. 왜냐하면 따옴표는 문자열의 일부로 포함되기 때문이다.

 매크로와 그 정의는 매우 길게 표시할 수 있다. 매크로 행 역시 명령 행과 마찬가지로 행의 마지막에 역슬래시(\)를 사용하여 행이 다음 줄로 계속 이어진다고 표시할 수 있다. make 가 매크로 정의를 명령이나 종속 항목 행 등과 확실하게 구별토록 하려면, 매크로의 이름 앞에 탭 문자를 두거나, 등호(=) 앞에 콜론(:)을 두어서는 안된다.

- 공백 처리

make에서 모든 문자열의 최종 위치는 셸 명령으로 사용하는 것이므로, 여백 문자(화이트 스페이스)들은 별다른 의미 없이 처리된다. 등호 바로 왼쪽 또는 오른쪽의 공백이나 탭 문자들은 모두 제거된다. 행을 계속 이어가기 위해 역슬래시를 사용하는 경우, make는 이를 하나의 공백 문자로 치환한다. 이와 같은 특별한 위치를 제외하면, make 는 문자열 내의 모든 공백과 탭 문자를 보존한다.

- 선언 및 정의 규칙

 매크로 이름은 보통 대문자로 구성되는데, 특별히 규정이 있는 것이 아니라 관습적으로 그렇게 사용하는 것뿐이다. 매크로는 대소문자나 숫자 심지어 밑줄 등을 자유롭게 조합하여 이름을 지을 수 있다.

 등호(=) 표시 이후에 아무런 문자열이 없는 매크로 정의에는 널(null) 문자열이 할당된다.

 매크로를 정의하는 순서 역시 중요하지 않다. 따라서 매크로를 하나씩 차곡차곡 정의해나가는 작업은 전혀 어렵지 않다:

SOURCES = ${MY_SRC} ${SHARED_SRC}
MY_SRC = parse.c search_file.c
SHARED_DIR = /users/b_proj/src
SHARED_SRC = ${SHARED_DIR}/depend.c

 위의 정의를 어떤 순서대로 지정하더라도, make${SOURCES} 를 아래처럼 평가한다:

parse.c search_file.c /users/b_proj/src/depend.c

 그러나 주의해야 할 점은 매크로는 반드시 사용할 항목보다 먼저 정의 해야 한다. 어떤 식으로든 일단 매크로 하나를 파일의 일부분에서 정의하고 나면 이를 다시 정의할 수는 없다. make 는 항상 파일에서 마지막 정의만을 참조로 사용한다.

- 참조 방식

 매크로를 참조할 때, 즉 make가 매크로를 그 정의로 치환토록 할 때는 매크로를 괄호(())나 중괄호({})로 둘러싸고, 그 앞에 달러 표시($)를 붙인다.

 문자 한 자로 이뤄진 매크로 이름에는 괄호나 중괄호를 사용할 필요가 없다. 예를 들어 아래와 같이 매크로 이름을 정의하면 참조 $A$(A) 그리고 ${A} 는 모두 동일하다.

A = XYZ

 매크로를 정의하지 않고 참조할 경우, make 는 그를 널 문자열로 치환한다. 따라서 정의하지 않은 매크로를 사용하였다 해서 make 가 오류 메세지를 나타내는 경우는 절대 생기지 않는다.

2. 내부적으로 정의된 매크로

make 는 수많은 일반 명령들을 매크로로 미리 정의해 놓고 있다. 예를 들어 make${CC} 매크로를 항상 C 컴파일러로, ${LD} 매크로를 항상 링커로 인식한다. 이처럼 사전에 정의해 놓은 매크로가 지닌 가장 큰 장점은 확장자 규칙과 함께 사용할 수 있다는 점이다. (확장자 규칙은 제 3장에서 자세히 알아본다.)

따라서 다음의 예시는

basic.o: basic.c
	${CC} -c basic.c

아래처럼 직접 입력한 경우의 결과와 대개 동일하다:

basic.o: basic.c
	cc -c basic.c

 또한 make 는 스스로 매크로로 정의한 각 명령과 관련된 또 다른 매크로를 정의하여 그 명령의 옵션을 확보하기도 한다. 프로그래머들은 대개 C 컴파일러의 옵션은 주로 CFLAGS 에, ld 의 옵션은 주로 LDFLAGS 에 저장한다.

3. 커맨드 라인에서 매크로 정의

make 커맨드 라인에서 매크로를 정의할 수 있다:

make jgref DIR=/usr/proj

make/usr/proj 문자열을 DIR 에 할당하며, 그런 다음에는 해당 기술 파일이 이 문자열을 ${DIR}로 액세스하게 된다. 보통은 커맨드 라인에서 타깃이 매크로 정의 앞에 위치하지만, 그런 순서에 별다른 의미는 없다. 이는 등호(=) 표시가 인수를 매크로 정의로 표시하기 때문이다.

 커맨드 라인에서 정의가 여러 단어들로 구성되어 있을 경우, 해당 부분을 작은 따옴표('')나 큰따옴표("")로 묶어줘야 한다. 물론 이는 단순히 셸 구문의 문제일 뿐이지만, 그렇게 하면 따옴표는 셸이 매크로 정의를 make에 하나의 인수로 전달하도록 보장해준다:

make jgref "DIR=/usr/proj /usr/lib /usr/proj/lib"

 또 하나 잊지 말아야 할 것은 본 셸(Bourne Shell)이나 콘 셸(Korn Shell)에서는 아래 예제처럼 매크로를 make 명령 이전에 정의할 수도 있다는 점이다:

DIR=/usr/proj make jgref

 여기서는 순서에 따라 make 가 셸의 정의들을 덮어 쓸 수 있는지 여부에 약간씩 차이가 난다.

4. 셸 변수

make를 실행할 때 환경 설정을 하는 셸 변수를 기술 파일에서 매크로로 사용할 수 있다. 하지만 한 자 이상의 문자로 구성된 이름일 경우에는 앞에서 설명한 것처럼 괄호나 중괄호를 사용하여 참조해야 한다. 따라서 make 를 실행하기 전에 아래 예제처럼 변수 정의를 셸에서 입력한다면

DIR=/usr/proj; export DIR
make jgref

아래와 같이 DIR을 기술 파일에서 사용할 수 있다:

SRC = $(DIR)/src
jgref:
	cd ${DIR}; ...

 위 명령들은 본 셸은 물론 콘 셸에서도 아무런 문제없이 동작한다. C 셸 사용자는 아래 예제처럼 setenv 명령을 사용하여 셸 변수들을 환경에 추가할 수 있다:

setenv DIR /usr/proj

 따라서 개발자가 .profile 이나 .login 등의 파일에서 설정하거나 혹은 다른 방법을 사용하여 설정한 환경 변수는 모두 기술 파일 내에서 사용할 수 있다.

주의: 매크로로 사용할 수 있는 이들 환경 변수는 모두 기술 파일에서 사용할 수 있는 반면, 동적으로 할당된 셸 변수는 사용하는 데 제한이 있다는 점에서 차이가 난다는 사실이다. 동적으로 할당된 셸 변수는 중괄호나 괄호 없이 단지 달러 기호 두 개($$)만으로 표시할 수 있지만, 행이 새로 바뀌면 효력이 없어진다.

5. 매크로 할당 우선순위

make 는 다음과 같은 우선순위에 따라 매크로를 할당한다:

  1. make 명령 입력 시 make 명령 다음에 입력한 매크로
  2. 기술 파일의 매크로 정의
  3. 현재 셸 환경 변수. 여기에는 사용자가 make 커맨드 라인에 입력할 때 명령 바로 앞에 입력한 매크로도 포함된다. (본 셸 및 콘 셸에만 해당)
  4. make 의 내부(기본) 정의

 결국 make 를 실행시켰을 때 실행될 내용을 궁극적으로 결정하는 것은 기술 파일이 되는 셈이다. 다시 말해 사용자가 기술 파일에서 볼 수 있는 것은 그에 비해 상대적으로 숨어 있는 셸 환경이나 기본 정의로 인해 무효화하는 사태는 벌어지지 않으며, 오직 make 커맨드 라인에서 사용자가 직접 입력한 명령에 의해서만 무효가 된다.

- 환경 변수를 우선하기

 기술 파일보다 환경 변수를 우선적으로 고려해야 하는 경우가 없을 수는 없다. 예를 들어, 프로젝트에서 지정한 여러 기본 설정을 개발자 자신이 원하는 설정으로 바꾸고 싶을 경우 그래서 make 를 반복해서 실행해야 하는 상황을 가정해 보자.

 이런 상황에서 가장 간단한 해결책은 개발자 자신의 환경 변수에 매크로를 정의한 다음, 억지로 기술 파일보다 우선적으로 참고도록 하는 것이다. make-e 옵션으로 실행시키면 우선순위가 다음과 같이 변경된다:

  1. make 명령 입력시 make 명령 다음에 입력한 매크로
  2. 현재 셸 환경 변수. 여기에는 사용자가 make 커맨드 라인에 입력할 때 명령 바로 앞에 입력한 매크로도 포함된다 (본 셸 및 콘 셸에만 해당).
  3. 기술 파일의 매크로 정의
  4. make 의 내부(기본) 정의

6. 매크로 문자열 치환

SRC = defs.c redraw.c calc.c

위와 같이 매크로를 정의했을 때 아래와 같은 기술 파일 명령을 내리면,

ls ${SRCS:.c=.o}

마치 이들 파일들이 존재하는 것처럼 다음과 같은 결과를 출력한다:

calc.o defs.o redraw.o

 그런데 문자열 치환은 매우 엄격하게 제한된다. 즉 매크로의 마지막 부분이나 공백 문자 바로 앞까지만 적용된다. 다시 말해, 기술 파일 항목이 아래와 같으면,

LETTER = xyz xyzabc abcxyz
...
	echo ${LETTERS:xyz=DEF}

다음과 같은 결과를 출력하는데 이는 abc 가 두 번째 xyz 의 존재를 숨기기 때문이다.

DEF xyzabc abcDEF

SOURCES = src1 src2.c glob.c
EXEC = $(SOURCES:.c=)

치환에서 두 번째 문자열은 널 문자열이 될 수 있지만, 첫 번째 문자열을 불가능하다. 따라서 위 경우 ${EXECS} 는 다음과 같이 평가된다:

src1 src2 glob

make 의 일부 버전에서는 위의 예보다 좀더 강력한 형태로 매크로 문자열 치환이 이뤄지기도 한다. 결국 문자열 치환 기능은 make의 버전에 따라 천차만별인 셈이다. 따라서 이 기능을 확인하려면 먼저 사용 중인 make 버전의 문서를 확인해보기 바란다. 일반적인 관행은 퍼센트(%)기호를 특정 문자열에 대응시키는 것이다.

7. 필요 항목과 타깃용 내부 매크로

make 는 종속 행을 읽을 때면 언제나 내부의 여러 매크로들을 정의한다. 이로 인해 사용자의 기술 파일이 간단해질 뿐 아니라, 이를 이용하면 항목을 쉽게 추가하거나 변경할 수 있다.

- $@ 매크로

$@ 매크로는 현재 타깃으로 간주된다. 이 매크로는 기술 파일들에서 아주 일반적으로 사용된다. 타깃이 대개 사용자가 빌드하려는 파일의 이름이기 때문이다. 따라서 실행 프로그램을 컴파일하는 항목은 일반적으로 @ 을 다음과 같이 출력 파일의 이름으로 사용한다:

plot_prompt: basic.o prompt.o
	cc -o $@ basic.o prompt.o

- $? 매크로

$? 매크로는 현재 타깃보다 최신인 필요 항목들의 명단으로 간주된다. 예를 들어, interact.o, sched.o, gen.o 등 오브젝트 파일 세 개를 지닌 libops 란 라이브러리가 있다고 가정해보자. 여기서 아래 기술 파일 항목은 라이브러리 자신보다 새로운 오브젝트 파일들을 사용하여 라이브러리를 다시 빌드한다:

libops: interact.o sched.o gen.o
	ar $@ $?

libops 를 처음 빌드하면 모든 오브젝트 모듈은 $? 에 치환되는데, 이는 make 가 존재하지 않는 타깃을 오래됐다고 간주하기 때문이다. 그리고 나중에 sched.o 를 다시 컴파일 하면, 이 모듈만이 라이브러리에서 유일하게 치환된다.

- 다중 타깃

 기술 파일에 있는 하나의 항목이 서로 다른 여러 형태의 타깃이나 필수 항목에서 여러 번 실행될 수 있다. 예를 들어 아래와 같은 항목이 있다고 가정해 보자:

new_spec new_imp: menus hash store
	date >> $@
	ls $? >> $@

 프로젝트 팀원들 가운데 누군가가 어제 menus 를 변경하고 new_impl 을 다시 빌드하였고, 오늘 다른 팀원이 store 를 변경하였을 경우, menusstore 는 모두 new_spec 보다 최신 파일이다. 그러나 new_impl 에게는 둘 가운데 store 만이 새로운 필요 항목이다. 이제 두 타깃을 다시 빌드하면 아래와 같다:

make new_spec
date >> new_sepc
ls menus store >> new_spec

make new_impl
date >> new_impl
ls store >> new_impl

- 종속 행 매크로 $$@

$@의 변형으로 달러 기호를 둘 가진 $$@ 이 있다. 이 매크로는 종속 행에서만 의미를 지닌다. 다시 말해 필요 항목을 지정하는 데만 사용된다는 얘기이다. 이에 반해 $?$@ 는 명령 행에서만 사용할 수 있다.

$$@$@ 과 동일한 내용, 즉 현재의 타깃을 참조한다. 달러 기호가 두 개 필요한 것은 make 가 기술 파일에서 읽고 해석하는 순서 때문이다:

docmk: $$@.c

따라서 위의 종속 행은 아래와 같이 평가된다:

docmk: docmk.c

 이는 하나의 소스 파일을 갖는 실행 파일들의 수가 많을 때 이들을 빌드하는 데 유용하다. 예를 들어 유닉스 시스템 명령들을 빌드하는데 소스 디렉토리는 종종 아래와 같은 기술 파일을 갖는다:

CMDS = cat dd echo date cccmp comm. Ar ld chown
${CMDS}: $$@.c
	${CC} -O $? -o $@

여기까지의 내용을 요약해서 간단하게 makefile 을 작성하면 다음과 같다:

C_OBJS = main.o iodat.o dorun.o
ASM_OBJS = lo.o
OBJS = ${C_OBJS} ${ASM_OBJS}
LIB = /usr/proj/lib/crtn.a

program: ${OBJS} ${LIB}
	${CC} -o $@ ${OBJS} ${LIB}
    
${ASM_OBJS}: $${@:.o=.s}
	${AS} -o $@ $?
    
${C_OBJS}: $${@:.o=.c}
	${CC} -c $?

출처

[책] make: 유닉스, 리눅스 필수 유틸리티 (앤드류 오람, 스티브 탈보트 저; 이석주 역)

profile
2000.11.30

0개의 댓글