1991년 어느 날, David MacKenzie라는 개발자는 Makefile을 37번째 수정하고 있었습니다.
그는 C 프로그램을 작성했고, 자신의 Linux 머신에서는 완벽하게 컴파일되고 실행되었습니다. 하지만 코드를 동료에게 보냈을 때 문제가 발생했습니다.
동료는 SunOS를 사용 중이었고, SunOS의 C 라이브러리에는 strerror() 함수가 없었습니다.
그는 #ifdef를 추가해 우회했습니다. 그리고 다른 동료에게 코드를 보냈습니다.
이번에는 HP-UX에서 컴파일이 실패했습니다. HP-UX의 string.h와 strings.h 헤더 파일 관계가 Linux와 달랐기 때문입니다.
그는 또 다른 #ifdef를 추가했습니다.
이후 AIX, IRIX, Ultrix, BSD, Minix... 모든 시스템에는 각자의 독특한 특성이 있었고, 매번 다른 #ifdef가 필요했습니다.
그의 코드는 점점 다음과 같은 형태가 되었습니다:
#ifdef HAVE_STRERROR
msg = strerror(errno);
#else
msg = "Unknown error";
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#else
#ifdef HAVE_STRINGS_H
#include <strings.h>
#endif
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif
코드의 절반은 로직이고, 나머지 절반은 #ifdef였습니다.
더 큰 문제는 누가 HAVE_XXX 매크로를 정의하느냐는 것이었습니다. 컴파일 전에 대상 시스템에 strerror가 있는지, string.h가 있는지, unistd.h가 있는지 등을 감지하고, 그 결과에 따라 config.h 파일을 생성해 해당 매크로를 정의해야 했습니다.
David MacKenzie는 이 상황에 지쳤습니다. 그는 이러한 감지 작업을 자동으로 수행하는 도구를 만들기로 결심했습니다. 그 도구가 바로 Autoconf입니다.
Autoconf의 정의
Autoconf는 configure.ac(감지 요구 사항 목록)를 configure(감지 실행 스크립트)로 변환하는 도구입니다.
비유를 들어 설명하자면, 집을 리모델링하기 전에 상황을 파악해야 합니다. 벽이 벽돌인지 콘크리트인지, 전선이 구리인지 알루미늄인지 등을 확인해야 합니다. 매번 다른 집을 리모델링할 때마다 모든 것을 처음부터 확인할 수는 없습니다. 따라서 감지 목록을 작성하고, 감지원에게 맡겨 각 집을 검사한 후 보고서를 받는 것이 효율적입니다.
이 비유에서 각 요소는 다음과 같습니다:
감지 목록 = configure.ac (개발자가 작성)
감지원 = autoconf (도구 자체)
보고서 생성기 = configure (autoconf가 생성한 스크립트)
보고서 = config.h + Makefile (configure가 생성한 결과물)
검사 대상 집 = 대상 운영체제
전체 워크플로우
개발자가 작성한 파일과 Autoconf가 생성한 파일, 그리고 configure가 실행된 후 생성되는 파일의 관계는 다음과 같습니다:
configure.ac ──→ [autoconf] ──→ configure ──→ [실행] ──→ config.h
Makefile
config.status
config.log
configure.ac 파일 분석
configure.ac는 Autoconf에 대한 "요구 사항 목록"이며, M4 매크로 언어로 작성됩니다. Mono 프로젝트의 configure.ac를 간략화한 예시입니다:
# ============================================
# 프로젝트 기본 정보
# ============================================
AC_INIT([mono], [6.12.0], [mono-bugs@lists.dot.net])
# 프로젝트 이름: mono, 버전: 6.12.0, 버그 리포트 이메일
AC_CONFIG_SRCDIR([mono/mini/mini.c])
# 소스 디렉토리 검증 파일 지정
AM_INIT_AUTOMAKE([foreign])
# Automake 초기화, foreign 모드 사용
# ============================================
# 컴파일 도구 감지
# ============================================
AC_PROG_CC
# C 컴파일러 찾기 (gcc, cc, cl 등 순서로 시도)
AC_PROG_CXX
# C++ 컴파일러 찾기
AC_PROG_INSTALL
# install 명령어 찾기
AC_PROG_LN_S
# 심볼릭 링크 지원 여부 확인
# ============================================
# 헤더 파일 감지
# ============================================
AC_CHECK_HEADERS([sys/mman.h])
# sys/mman.h 헤더 존재 여부 확인, 존재 시 HAVE_SYS_MMAN_H 정의
AC_CHECK_HEADERS([sys/socket.h netinet/in.h])
# 네트워크 관련 헤더 확인
AC_CHECK_HEADERS([pthread.h])
# POSIX 스레드 헤더 확인
# ============================================
# 라이브러리 및 함수 감지
# ============================================
AC_CHECK_LIB([pthread], [pthread_create])
# pthread 라이브러리 존재 여부 확인
AC_CHECK_FUNCS([strerror mmap getpagesize sysconf])
# 각 함수 존재 여부 확인, 존재 시 HAVE_STRERROR 등 정의
AC_CHECK_FUNCS([dlopen], [], [
AC_CHECK_LIB([dl], [dlopen])
])
# dlopen 함수 확인, 없으면 libdl 라이브러리에서 찾기
# ============================================
# 서드파티 라이브러리 감지
# ============================================
PKG_CHECK_MODULES(GLIB, glib-2.0 >= 2.28)
# pkg-config로 GLib 라이브러리 확인, GLIB_CFLAGS와 GLIB_LIBS 변수 설정
# ============================================
# 시스템 특성 감지
# ============================================
AC_C_BIGENDIAN
# CPU 엔디언(빅엔디언/리틀엔디언) 확인
AC_CHECK_SIZEOF([void *])
# 포인터 크기 확인 (32비트: 4바이트, 64비트: 8바이트)
AC_SYS_LARGEFILE
# 대용량 파일(2GB 초과) 지원 여부 확인
# ============================================
# 출력 파일 생성
# ============================================
AC_CONFIG_HEADERS([config.h])
# 모든 감지 결과를 config.h에 기록
AC_CONFIG_FILES([
Makefile
mono/Makefile
mono/mini/Makefile
])
# .in 템플릿 파일로부터 Makefile 생성
AC_OUTPUT
# 출력 실행, 모든 파일 생성
AC_ 매크로의 내부 동작
AC_CHECK_HEADERS([sys/mman.h]) 매크로가 Autoconf에 의해 처리되면, 최종 configure 스크립트에 다음과 같은 Shell 코드가 생성됩니다:
# configure 스크립트 내부 코드 (단순화)
echo -n "checking for sys/mman.h... "
# 임시 C 소스 파일 생성
cat > conftest.c << EOF
#include
int main() { return 0; }
EOF
# 컴파일 시도
if $CC -c conftest.c -o conftest.o 2>/dev/null; then
echo "yes"
# 컴파일 성공: 헤더 파일 존재
echo "#define HAVE_SYS_MMAN_H 1" >> config.h
else
echo "no"
# 컴파일 실패: 헤더 파일 없음
echo "/* #undef HAVE_SYS_MMAN_H */" >> config.h
fi
# 임시 파일 정리
rm -f conftest.c conftest.o
감지 방법은 매우 직접적입니다. 작은 프로그램을 작성해 컴파일을 시도하고, 성공 여부로 해당 기능의 존재 여부를 판단합니다. AC_CHECK_LIB([pthread], [pthread_create])의 경우도 비슷한 방식으로 작동합니다:
echo -n "checking for pthread_create in -lpthread... "
cat > conftest.c << EOF
extern int pthread_create();
int main() {
pthread_create();
return 0;
}
EOF
# -lpthread 옵션으로 컴파일 및 링크 시도
if $CC conftest.c -o conftest -lpthread 2>/dev/null; then
echo "yes"
LIBS="$LIBS -lpthread"
else
echo "no"
fi
rm -f conftest.c conftest
Autoconf의 작동 과정: 매크로에서 스크립트로
Autoconf의 핵심 엔진은 M4 매크로 프로세서입니다. M4는 텍스트 치환 방식으로 작동합니다. Autoconf는 수백 개의 AC_ 접두사가 붙은 매크로를 사전 정의하며, 각 매크로는 세심하게 작성된 Shell 스크립트 템플릿으로 확장됩니다.
전체 확장 과정은 다음과 같습니다:
configure.ac (개발자가 작성, 수백 줄)
↓ autoconf (M4 매크로 확장)
configure (생성됨, 수만 줄)
autoconf 명령을 실행하면 디렉토리에 configure 파일이 생성됩니다. 이 파일은 완전히 독립적이며, Autoconf나 M4와 같은 특수 도구에 의존하지 않고 기본 Shell(/bin/sh)만으로 실행 가능합니다. 이는 Autoconf의 중요한 설계 철학입니다: 생성된 configure 스크립트는 가장 기본적인 시스템에서도 실행되어야 한다는 것입니다.
configure 스크립트 실행
./configure --prefix=/usr/local/mono 명령을 실행하면 화면에 수많은 감지 정보가 스크롤됩니다:
checking build system type... x86_64-pc-linux-gnu
checking host system type... x86_64-pc-linux-gnu
checking for gcc... gcc
checking whether the C compiler works... yes
checking for sys/mman.h... yes
checking for strerror... yes
checking for glib... yes
checking size of void *... 8
checking whether byte ordering is bigendian... no
...
각 checking for XXX... yes/no는 하나의 감지 항목입니다.
감지가 완료되면 configure는 config.h 파일을 생성합니다:
/* config.h - configure에 의해 자동 생성됨, 수정 금지 */
/* 시스템 특성 */
#define SIZEOF_VOID_P 8
#define WORDS_BIGENDIAN 0
/* 헤더 파일 */
#define HAVE_SYS_MMAN_H 1
#define HAVE_PTHREAD_H 1
/* 함수 */
#define HAVE_STRERROR 1
#define HAVE_MMAP 1
#define HAVE_DLOPEN 1
/* 패키지 정보 */
#define PACKAGE "mono"
#define VERSION "6.12.0"
소스 코드는 이 config.h 파일을 포함하여 다음과 같이 사용할 수 있습니다:
// mini-posix.c 예시
#include "config.h"
#ifdef HAVE_SYS_MMAN_H
#include <sys/mman.h>
void* allocate_code_memory(size_t size) {
return mmap(NULL, size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
}
#else
void* allocate_code_memory(size_t size) {
return malloc(size);
}
#endif
#ifdef HAVE_DLOPEN
#include <dlfcn.h>
void* load_native_library(const char *name) {
return dlopen(name, RTLD_LAZY);
}
#else
#error "No dynamic library loading support!"
#endif
config.h는 "시스템 감지"와 "조건부 컴파일"을 연결하는 다리 역할을 합니다.
왜 configure 스크립트를 직접 작성하지 않는가?
세 가지 주요 이유가 있습니다:
- 복잡성:
AC_CHECK_HEADERS([sys/mman.h])같은 단일 매크로가 수십 줄의 Shell 코드로 확장되며, 다양한 경계 조건과 시스템 차이를 처리합니다. 직접 작성할 경우 누락된 상황이 발생하기 쉽습니다. - 이식성: 시스템마다 Shell의 동작 방식이 다릅니다. Autoconf가 생성하는 스크립트는 POSIX 표준을 엄격히 준수하여 어떤 시스템에서도 실행 가능합니다.
- 유지보수성:
configure.ac는 선언적이어서 "무엇을 확인해야 하는지"만 명시하고, "어떻게 확인하는지"는 명시하지 않습니다. 이는 간결하고 가독성이 뛰어나며 유지보수가 쉽습니다.
Autoconf는 1991년에 탄생하여 30년 이상 수많은 오픈소스 프로젝트의 크로스 플랫폼 컴파일을 지원해왔습니다. Linux 커널의 도구 체인, GCC 컴파일러, Python 인터프리터, Apache 서버, OpenSSL 암호화 라이브러리, FFmpeg 멀티미디어 프레임워크 등이 모두 Autoconf의 도움을 받았습니다. 이 도구는 개발자가 코드 작성에 집중하고 각 시스템의 독특한 특성과 싸우지 않도록 도와줍니다.