책에서 등장한 명령 행
이란 단어는 문맥에 맞게 커맨드 라인
혹은 명령 행
이라고 옮겼다. 전부 다 명령 행이라고 하니까 기술 파일의 명령 행인지 프롬프트에서의 커맨드 라인인지 도통 알 수가 없다.
타깃들이 수십 개의 파일들에 종속되거나 여러 개의 서로 다른 버전으로 만들어지기 마련인 실제 프로젝트의 기술 파일에서 반복되는 텍스트의 양을 실로 엄청나리라 생각할 수 있으나 실제 사용되는 기술 파일을 열어보면 뜻밖에도 보통 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
매크로 정의는 등호(=
)를 포함하는 하나의 문장(행)이다. 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
가 오류 메세지를 나타내는 경우는 절대 생기지 않는다.
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
에 저장한다.
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
가 셸의 정의들을 덮어 쓸 수 있는지 여부에 약간씩 차이가 난다.
make
를 실행할 때 환경 설정을 하는 셸 변수를 기술 파일에서 매크로로 사용할 수 있다. 하지만 한 자 이상의 문자로 구성된 이름일 경우에는 앞에서 설명한 것처럼 괄호나 중괄호를 사용하여 참조해야 한다. 따라서 make
를 실행하기 전에 아래 예제처럼 변수 정의를 셸에서 입력한다면
DIR=/usr/proj; export DIR
make jgref
아래와 같이 DIR
을 기술 파일에서 사용할 수 있다:
SRC = $(DIR)/src
jgref:
cd ${DIR}; ...
위 명령들은 본 셸은 물론 콘 셸에서도 아무런 문제없이 동작한다. C 셸 사용자는 아래 예제처럼 setenv
명령을 사용하여 셸 변수들을 환경에 추가할 수 있다:
setenv DIR /usr/proj
따라서 개발자가 .profile
이나 .login
등의 파일에서 설정하거나 혹은 다른 방법을 사용하여 설정한 환경 변수는 모두 기술 파일 내에서 사용할 수 있다.
주의: 매크로로 사용할 수 있는 이들 환경 변수는 모두 기술 파일에서 사용할 수 있는 반면, 동적으로 할당된 셸 변수는 사용하는 데 제한이 있다는 점에서 차이가 난다는 사실이다. 동적으로 할당된 셸 변수는 중괄호나 괄호 없이 단지 달러 기호 두 개(
$$
)만으로 표시할 수 있지만, 행이 새로 바뀌면 효력이 없어진다.
make
는 다음과 같은 우선순위에 따라 매크로를 할당한다:
make
명령 입력 시 make
명령 다음에 입력한 매크로make
커맨드 라인에 입력할 때 명령 바로 앞에 입력한 매크로도 포함된다. (본 셸 및 콘 셸에만 해당)make
의 내부(기본) 정의 결국 make
를 실행시켰을 때 실행될 내용을 궁극적으로 결정하는 것은 기술 파일이 되는 셈이다. 다시 말해 사용자가 기술 파일에서 볼 수 있는 것은 그에 비해 상대적으로 숨어 있는 셸 환경이나 기본 정의로 인해 무효화하는 사태는 벌어지지 않으며, 오직 make
커맨드 라인에서 사용자가 직접 입력한 명령에 의해서만 무효가 된다.
기술 파일보다 환경 변수를 우선적으로 고려해야 하는 경우가 없을 수는 없다. 예를 들어, 프로젝트에서 지정한 여러 기본 설정을 개발자 자신이 원하는 설정으로 바꾸고 싶을 경우 그래서 make
를 반복해서 실행해야 하는 상황을 가정해 보자.
이런 상황에서 가장 간단한 해결책은 개발자 자신의 환경 변수에 매크로를 정의한 다음, 억지로 기술 파일보다 우선적으로 참고도록 하는 것이다. make
에 -e
옵션으로 실행시키면 우선순위가 다음과 같이 변경된다:
make
명령 입력시 make
명령 다음에 입력한 매크로make
커맨드 라인에 입력할 때 명령 바로 앞에 입력한 매크로도 포함된다 (본 셸 및 콘 셸에만 해당).make
의 내부(기본) 정의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
버전의 문서를 확인해보기 바란다. 일반적인 관행은 퍼센트(%
)기호를 특정 문자열에 대응시키는 것이다.
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
를 변경하였을 경우, menus
와 store
는 모두 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: 유닉스, 리눅스 필수 유틸리티 (앤드류 오람, 스티브 탈보트 저; 이석주 역)