빌드 시스템이 푸는 문제는 하나다. 무엇을 다시 만들어야 하는가. 소스가 두 개면 전부 다시 컴파일하면 그만이지만, 파일이 수천 개가 되고 컴파일러·플랫폼·외부 라이브러리가 얽히면 이 질문이 프로젝트 생산성을 좌우한다.

이 글은 도구 사용법이 아니라 계보를 본다. make가 무엇을 해결했고, 왜 그것으로 부족해졌고, CMake가 무엇을 바꿨고, 지금 나오는 도구들은 또 무엇을 노리는가. 각 도구의 실전 사용은 이 묶음의 다음 글들에서 다룬다.

make (1976) — 다시 만들지 말지를 기계가 판단한다

make는 1976년 벨 연구소의 Stuart Feldman이 만들었고, 1979년 논문으로 정리돼 나왔다. 발상은 지금 봐도 간결하다.

  • 사람은 의존 관계를 적는다 — 이 결과물은 저 파일들로 만들어진다
  • 기계는 타임스탬프를 본다 — 결과물보다 새로운 입력이 있으면 다시 만든다
main.o: main.c config.h      # 타겟: 의존성
	cc -c main.c              # 레시피 (앞은 탭)

세 줄 구조가 반세기 가까이 살아남은 이유는 이 판단 규칙이 언어와 무관하기 때문이다. C를 컴파일하든 문서를 생성하든 “입력이 결과보다 새로우면 다시 만든다”는 규칙은 그대로 성립한다.

문제는 레시피 자리에 실행할 명령을 그대로 적는다는 점이다. 여기가 나중에 발목을 잡는다.

Autotools (1991~) — 이식성을 스크립트로 때우다

Makefile에 cc -c main.c라고 적는 순간, 그 Makefile은 그 환경에 묶인다. 컴파일러 이름이 gcc인지 cc인지, 스레드에 -lpthread가 필요한지, 헤더가 어디 있는지를 파일 스스로 알 방법이 없다.

autoconf(1991, David MacKenzie)와 뒤이은 automake·libtool — 통칭 Autotools — 가 내놓은 답은 “빌드 전에 시스템을 조사한다”였다. 유닉스 소스 배포판에서 익숙한 그 절차다.

./configure && make && make install

configure가 실제로 시험 컴파일을 해 가며 이 헤더가 있는지, 이 함수가 링크되는지를 확인하고, 그 결과를 반영한 Makefile을 생성한다. “생성”이라는 개념이 여기서 처음 자리를 잡는다.

대가도 분명했다.

  • M4 매크로가 셸 스크립트를 만들어 내는 구조라 읽기도 고치기도 어렵다
  • 생성된 configure는 수만 줄이고 매번 느리게 돈다
  • 무엇보다 POSIX 셸이 전제다. Visual Studio와 MSVC를 쓰는 윈도우 개발자는 이 흐름에 낄 수 없다

CMake (2000) — 빌드하지 않고 빌드 파일을 만든다

CMake는 Kitware의 Bill Hoffman이 만들었다. 공식 소개는 계기를 이렇게 적고 있다 — “He created CMake in response to the need for a powerful, cross-platform build environment for The Insight Toolkit (ITK) and the Visualization Toolkit (VTK).” 의료영상·시각화 라이브러리를 유닉스와 윈도우 양쪽에서 빌드해야 했는데, Autotools로는 윈도우가 막혔다.

전환점은 CMake 자신이 빌드를 하지 않는다는 점이다. CMake는 각 플랫폼의 네이티브 빌드 환경을 생성한다.

CMakeLists.txt ─┬─→ Unix Makefiles
                ├─→ Visual Studio 솔루션
                ├─→ Xcode 프로젝트
                └─→ build.ninja

말로만 하는 이야기가 아니라 cmake --help에 그대로 나온다. 같은 CMake라도 플랫폼에 따라 고를 수 있는 목록이 다르다.

# Windows (CMake 4.2.1)
* Visual Studio 18 2026        = Generates Visual Studio 2026 project files.
  Visual Studio 17 2022        = Generates Visual Studio 2022 project files.
  NMake Makefiles              = Generates NMake makefiles.
  Ninja                        = Generates build.ninja files.
 
# WSL 우분투 (CMake 3.22.1)
  Unix Makefiles               = Generates standard UNIX makefiles.
  Ninja                        = Generates build.ninja files.
  Kate - Ninja / Sublime Text 2 - Ninja / Eclipse CDT4 - ...

프로젝트를 한 번만 기술하면, 각자 쓰던 도구를 그대로 쓴다. 유닉스 개발자는 make를, 윈도우 개발자는 Visual Studio를, CI는 Ninja를 돌린다. CMake가 사실상 표준이 된 이유는 문법이 예뻐서가 아니라 이 지점이었다.

모던 CMake (3.0, 2014) — 디렉터리에서 타깃으로

CMake도 한 번 갈아엎였다. 초기 CMake는 설정이 디렉터리 단위였다.

include_directories(/usr/local/include)   # 이 디렉터리 아래 모든 타깃에 적용
add_definitions(-DUSE_FOO)

이러면 라이브러리를 쓰는 쪽이 그 라이브러리에 필요한 설정을 직접 알아내 다시 적어야 한다. 3.0 이후의 이른바 모던 CMake는 이것을 타깃에 붙는 사용 요구사항(usage requirements) 으로 바꿨다.

target_include_directories(mylib PUBLIC include)   # mylib을 쓰는 쪽에도 자동 적용
target_link_libraries(myapp PRIVATE mylib)          # myapp 안에서만 쓰고 전파 안 함

PUBLIC/PRIVATE/INTERFACE의존성이 전이되게 만든 것이 실질적인 변화다. CMake가 어렵다는 인상의 상당 부분은, 인터넷에 옛 방식과 새 방식 예제가 뒤섞여 있어서 생긴다.

Ninja (2012) — 사람이 쓰지 않는 실행기

크롬 빌드에서 출발한 Ninja는 목표가 노골적이다. 공식 문서가 스스로를 이렇게 규정한다 — “Where other build systems are high-level languages, Ninja aims to be an assembler.” 그리고 손으로 쓰는 편의는 아예 비목표로 선언한다. “You should generate your ninja files using another program.”

설계 원칙도 한 줄로 적혀 있다 — “when convenience and speed are in conflict, prefer speed.”

여기서 구조가 갈린다. 기술(記述)은 CMake·Meson이 담당하고, 실행은 Ninja가 담당한다. 사람이 읽을 필요가 없으니 파서를 단순하게 유지할 수 있고, 그 덕에 증분 빌드에서 “할 일 없음”을 판정하는 속도가 빨라진다. 요즘 CMake 프로젝트가 기본으로 -G Ninja를 쓰는 이유다.

그다음 — 축이 재현성과 캐시로 옮겨 간다

도구무엇을 포기하고 무엇을 얻었나
Meson (2013)CMake의 표현력을 줄이는 대신 읽기 쉬운 문법과 빠른 설정. 백엔드는 Ninja. GNOME·systemd가 채택
Bazel (2015 공개)구글 내부 도구의 공개판. 문법 편의가 아니라 밀폐성·재현성·규모가 목표
언어 내장 (Cargo, go build)빌드와 의존성 관리를 아예 분리하지 않음. C++이 갖지 못한 것
패키지 계층 (vcpkg, Conan, FetchContent)“라이브러리를 어떻게 가져오나”를 빌드 기술과 분리해 뒤늦게 메움

Bazel이 무엇을 노리는지는 공식 소개에 분명히 적혀 있다. 샌드박스로 빌드와 테스트를 격리해 “minimizing skew and maximizing reproducibility”를 노리고, 규모에 대해서는 “Bazel maintains agility while handling builds with 100k+ source files”라고 말한다. 캐시에 대해서도 “Bazel caches all previously done work and tracks changes to both file content and build commands”라고 밝힌다.

핵심은 밀폐성(hermetic) 이다. 빌드가 호스트에 깔린 헤더나 환경변수에 의존하지 않으면 결과가 재현되고, 결과가 재현되면 남이 이미 빌드한 산출물을 그대로 받아 쓸 수 있다. 원격 캐시와 원격 실행은 그 위에 얹히는 기능이다. 수천 명이 한 저장소를 함께 쓰는 규모에서는 “빌드를 빠르게 하는 법”이 아니라 “빌드를 아예 안 하는 법”이 답이 된다.

흐름을 세 줄로

  1. 기술과 실행이 분리됐다 — 사람은 CMake·Meson으로 쓰고, 기계는 Ninja·Makefile을 돌린다
  2. 이식성은 생성기가 떠안았다 — 플랫폼별 Makefile을 손으로 관리하던 일이 사라졌다
  3. 새 경쟁축은 재현성과 캐시다 — 빌드를 빠르게 하는 것보다 다시 하지 않는 것이 이득이 크다

그래서 make는 죽었나

아니다. 위 도구 대부분이 결국 make나 ninja를 실행한다. 그리고 파일 몇 개짜리 프로젝트나 단발 작업 자동화에서는 여전히 make가 가장 적은 비용으로 붙는다.

갈아탈 신호는 분명하다. 여러 플랫폼, 여러 컴파일러, 외부 의존성 중 둘 이상이 들어오는 순간, 손으로 쓴 Makefile은 1991년에 Autotools가 풀려던 문제를 처음부터 다시 만나게 된다. 그때가 생성기로 옮길 때다.

이 시리즈가 계속 쓸 예제

말로 비교하면 어느 쪽이든 그럴듯해 보인다. 그래서 이 묶음은 같은 프로젝트를 도구만 바꿔 가며 빌드한다. 텍스트 통계를 내는 작은 CLI다.

textstat/
├── include/textstat.h        공용 헤더
├── src/count.c               줄·단어 세기
├── src/histogram.c           표준편차 (sqrt → -lm 필요)
├── src/platform_posix.c      디렉터리 나열 (POSIX)
├── src/platform_win32.c      디렉터리 나열 (Windows)
├── app/main.c                CLI 진입점
├── tests/test_count.c        자체 테스트
└── version.h.in              빌드 때 version.h로 생성

파일 여덟 개지만 빌드 시스템을 고르게 만드는 요소를 일부러 다 넣었다.

예제 요소이게 있어야 드러나는 것
공용 헤더증분 빌드와 헤더 의존성
라이브러리 + 실행 파일타깃 개념, 의존성 전파
-lm 링크플랫폼마다 다른 링크 플래그
OS별 소스 분기조건 분기를 누가 떠안는가
version.h 생성빌드 중 만들어지는 파일의 순서 문제
테스트 실행 파일테스트를 빌드 시스템이 아는가
디버그/릴리즈 두 구성구성 관리가 수작업인가 아닌가

다음 글에서는 이 프로젝트를 make로 처음부터 빌드하고, 위 항목들이 하나씩 어떻게 부담이 되는지까지 간다.

이 글의 CMake 제너레이터 목록은 이 작업 환경에서 직접 확인한 것이다(윈도우 CMake 4.2.1 / WSL2 우분투 CMake 3.22.1, GNU Make 4.3, Ninja 1.10.1·1.12.0). 연도·인물·설계 의도는 각 프로젝트의 공식 문서를 근거로 적었고, 인용문은 원문 그대로 옮겼다.

참고

  • CMake — Overview — Bill Hoffman이 ITK·VTK의 크로스 플랫폼 빌드 요구 때문에 CMake를 만들었다는 서술, 그리고 CMake가 네이티브 빌드 환경을 생성하는 도구라는 설명을 확인했다.
  • The Ninja build system — Manual — “aims to be an assembler”, “You should generate your ninja files using another program”, “prefer speed” 등 설계 원칙을 확인했다.
  • Bazel — Intro to Bazel — 샌드박스를 통한 재현성, 10만 개 이상 소스 파일 규모, 이전 작업 캐싱에 대한 서술을 확인했다.
  • Stuart Feldman, Make — A Program for Maintaining Computer Programs (1979) — make의 원래 설계 의도(의존 관계 선언 + 타임스탬프 비교)의 출처.
  • GNU Autoconf 매뉴얼configure가 시스템 기능을 시험해 빌드 설정을 만들어 내는 구조를 확인했다.