`make`로 C 프로그램 컴파일하기 (C 프로그래머가 아닌 사람들을 위해)
저는 C 프로그래머로 살아온 적은 없지만 가끔 소스에서 C/C++ 프로그램을 컴파일해야 할 때가 있습니다. 이게 꽤 애를 먹었는데요, 오랫동안 저의 방식은 기본적으로 “의존성을 설치하고 make를 실행해 보고, 안 되면 누군가 컴파일해 둔 바이너리를 찾거나 그냥 포기한다” 정도였습니다.
“누군가 컴파일해 줬겠지”라는 기대는 리눅스를 쓸 때는 꽤 잘 통했지만, 지난 몇 년간 맥을 쓰면서 직접 프로그램을 컴파일해야 하는 상황이 더 많아졌습니다.
그럼 C 프로그램을 컴파일할 때 어떤 일을 해야 하는지 이야기해 보겠습니다! 제가 직접 컴파일해 본 몇 가지 구체적인 C 프로그램 사례를 들어, 어떤 문제가 생길 수 있는지도 짚어보겠습니다. 앞으로 다룰 프로그램은 다음 세 가지입니다.
1단계: C 컴파일러 설치하기
이건 꽤 간단합니다. 우분투 시스템에서 C 컴파일러가 아직 없다면 다음 명령으로 설치합니다.
sudo apt-get install build-essential
이렇게 하면 gcc, g++, make가 설치됩니다. 맥에서는 상황이 좀 더 헷갈리는데, 대략 “Xcode 커맨드 라인 도구 설치” 같은 식입니다.
2단계: 프로그램의 의존성 설치하기
몇몇 최신 프로그래밍 언어와 달리, C에는 의존성 관리자가 없습니다. 그래서 프로그램에 의존성이 있다면 직접 찾아서 해결해야 합니다. 다행히 이런 이유 때문에 C 프로그래머들은 보통 의존성을 아주 최소한으로 유지하고, 필요한 의존성은 대개 사용 중인 패키지 매니저에서 바로 찾을 수 있습니다.
README에는 거의 항상 의존성을 어떻게 구하는지 설명하는 부분이 있습니다. 예를 들어 paperjam의 README에는 이렇게 나와 있습니다.
PaperJam을 컴파일하려면 libqpdf와 libpaper 라이브러리의 헤더가 필요합니다(보통 libqpdf-dev와 libpaper-dev 패키지로 제공됩니다).
매뉴얼 페이지를 빌드하려면
a2x(AsciiDoc에 포함)가 필요할 수 있습니다.
따라서 데비안 계열 시스템에서는 다음과 같이 의존성을 설치할 수 있습니다.
sudo apt install -y libqpdf-dev libpaper-dev
README에 패키지 이름(예: libqpdf-dev)이 나와 있다면, 기본적으로 “데비안 계열 리눅스 배포판 기준”이라고 생각하면 됩니다. 맥에서 brew install libqpdf-dev는 동작하지 않습니다. 저는 아직 맥에서의 개발에 완전히 익숙해지지 않아서 드릴 팁이 많지는 않습니다. 이 경우에는 Homebrew를 사용한다면 brew install qpdf가 될 것입니다.
3단계: ./configure 실행하기(필요한 경우)
일부 C 프로그램은 Makefile을 포함하고 있고, 다른 일부는 ./configure라는 스크립트를 대신 포함합니다. 예를 들어 sqlite 소스 코드를 다운로드하면 Makefile 대신 ./configure 스크립트가 들어 있습니다.
제가 이해한 ./configure 스크립트의 동작은 다음과 같습니다.
- 실행하면 다소 난해한 출력이 잔뜩 나온 뒤,
Makefile을 생성하거나 의존성이 없어서 실패합니다 ./configure스크립트는 autotools라는 시스템의 일부인데, 저는 “Makefile을 생성하기 위해 실행한다”는 것 이상은 배울 필요가 없었습니다.
./configure 스크립트에 옵션을 넘겨서 다른 Makefile을 만들게 할 수도 있는 것 같지만, 저는 한 번도 그렇게 해 본 적은 없습니다.
4단계: make 실행하기
다음 단계는 make를 실행해 프로그램을 빌드해 보는 것입니다. make에 대해 몇 가지 덧붙이자면 다음과 같습니다.
- 때로는
make -j8로 빌드를 병렬화해서 더 빠르게 만들 수 있습니다 - 프로그램을 컴파일할 때 보통 컴파일러 경고가 엄청나게 많이 출력됩니다. 저는 항상 그냥 무시합니다. 제가 만든 소프트웨어도 아니니까요! 컴파일러 경고는 제가 신경 쓸 문제가 아닙니다.
컴파일러 오류는 대개 의존성 문제입니다
맥에서 paperjam을 컴파일하다가 받은 오류는 다음과 같습니다.
/opt/homebrew/Cellar/qpdf/12.0.0/include/qpdf/InputSource.hh:85:19: error: function definition does not declare parameters
85 | qpdf_offset_t last_offset{0};
| ^
여러 해 동안 이런 문제를 너무 깊게 생각하지 않는 게 대체로 최선이라는 걸 배웠습니다. 오류 메시지에 qpdf가 언급된다면, 십중팔구 qpdf 의존성을 포함하는 방법을 잘못했기 때문입니다.
이제 qpdf 의존성을 올바르게 포함시키는 몇 가지 방법을 이야기해 보겠습니다.
컴파일러와 링커에 대한 세상에서 가장 짧은 소개
의존성 문제를 어떻게 해결하는지 이야기하기 전에, C 프로그램 빌드는 두 단계로 나뉜다는 점을 짚고 넘어가겠습니다.
- 컴파일 — 코드를 오브젝트 파일로 만드는 단계(
gcc나clang사용) - 링크 — 그 오브젝트 파일들을 최종 바이너리로 묶는 단계(
ld사용)
C 프로그램을 빌드할 때 이 구분을 아는 것이 중요합니다. 컴파일하려는 프로그램의 의존성을 어디서 찾아야 하는지 컴파일러와 링커에 올바른 플래그로 알려줘야 할 때가 있기 때문입니다.
make는 환경 변수로 컴파일러와 링커를 설정합니다
맥에서 paperjam을 설치하려고 make를 실행하면 이런 오류가 납니다.
c++ -o paperjam paperjam.o pdf-tools.o parse.o cmds.o pdf.o -lqpdf -lpaper
ld: library 'qpdf' not found
이 오류는 qpdf가 시스템에 설치되어 있지 않아서 생긴 것이 아닙니다(실제로 설치되어 있습니다!). 다만 컴파일러와 링커가 qpdf 라이브러리를 어디서 찾아야 하는지 모르는 것입니다. 이를 해결하려면 다음과 같이 해야 합니다.
- 컴파일러에
"-I/opt/homebrew/include"를 넘깁니다(헤더 파일을 어디서 찾을지 알려주기 위해) - 링커에
"-L/opt/homebrew/lib -liconv"를 넘깁니다(라이브러리 파일을 어디서 찾을지, 그리고iconv를 링크하라고 알려주기 위해)
그리고 이런 추가 파라미터를 환경 변수를 이용해 make가 컴파일러와 링커에 전달하도록 할 수 있습니다! 어떻게 동작하는지 보려면 paperjam의 Makefile 안에 있는 여러 환경 변수를 보면 됩니다. 예를 들어 여기 있는 LDLIBS처럼요.
paperjam: $(OBJS)
$(LD) -o $@ $^ $(LDLIBS)
LDLIBS 환경 변수에 넣은 모든 내용은 링커(ld)에 커맨드라인 인자로 전달됩니다.
숨겨진 환경 변수: CPPFLAGS
Makefile이 컴파일러/링커에 전달할 환경 변수를 직접 정의하기도 하지만, make에는 C 컴파일러와 링커에 자동으로 전달되는 여러 “암시적” 환경 변수도 있습니다. 여기에서 암시적 환경 변수의 전체 목록을 볼 수 있는데, 그중 하나가 C 컴파일러에 자동으로 전달되는 CPPFLAGS입니다.
(엄밀히 말하면 이럴 때는 CXXFLAGS를 쓰는 게 더 일반적이지만, 이 Makefile은 CXXFLAGS를 하드코딩해 두어서 Makefile을 수정하지 않고 컴파일러 플래그를 설정할 수 있는 유일한 방법이 CPPFLAGS를 설정하는 것이었습니다)
make가 C/C++와 얼마나 밀접하게 연결되어 있는지 깨닫는 데 오래 걸렸습니다. 예전에는 make가 그저 일반적인 빌드 시스템이라고 생각했는데(물론 무엇이든 빌드하는 데 쓸 수는 있습니다!), 다른 종류의 프로그램에는 없는 C/C++ 프로그램 빌드를 위한 여러 편의 기능이 있습니다.make에 환경 변수를 넘기는 두 가지 방법
@zwol 덕분에 make에 환경 변수를 넘기는 방법이 실제로 두 가지 있다는 걸 알게 되었습니다.
CXXFLAGS=xyz make(일반적인 방법)make CXXFLAGS=xyz
둘의 차이는 make CXXFLAGS=xyz는 Makefile 안에 설정된 CXXFLAGS 값을 덮어쓰지만, CXXFLAGS=xyz make는 그렇지 않다는 점입니다.
어느 쪽이 관례인지는 잘 모르겠지만, 이 글에서는 첫 번째 방법을 사용하겠습니다.
CPPFLAGS와 LDLIBS로 이 컴파일러 오류를 해결하는 방법
이제 CPPFLAGS와 LDLIBS가 컴파일러와 링커에 어떻게 전달되는지 이야기했으니, 프로그램을 성공적으로 빌드하기 위해 제가 최종적으로 사용한 주문을 공개하겠습니다!
CPPFLAGS="-I/opt/homebrew/include" LDLIBS="-L/opt/homebrew/lib -liconv" make paperjam
이렇게 하면 -I/opt/homebrew/include는 컴파일러에, -L/opt/homebrew/lib -liconv는 링커에 전달됩니다.
그리고 제가 마법처럼 올바른 인자를 알았던 것처럼 보이고 싶지는 않습니다. 그 인자들을 알아내는 과정에서는 이 글에서는 생략한 혼란스러운 구글링이 한참 있었습니다. 그래도 몇 가지는 말씀드리자면 다음과 같습니다.
-I컴파일러 플래그는/opt/homebrew/include/qpdf/QPDF.hh처럼 헤더 파일을 어느 디렉터리에서 찾을지 컴파일러에 알려줍니다-L링커 플래그는/opt/homebrew/lib/libqpdf.a처럼 라이브러리를 어느 디렉터리에서 찾을지 링커에 알려줍니다-l링커 플래그는 어떤 라이브러리를 링크할지 링커에 알려줍니다. 예를 들어-liconv는 “iconv라이브러리를 링크하라”는 뜻이고,-lm은 “math를 링크하라”는 뜻입니다
팁: 특정 파일 하나만 빌드하는 방법: make $FILENAME
어제 qf라는 멋진 도구를 발견했는데, ripgrep의 출력에서 파일을 빠르게 열 때 사용할 수 있습니다.
qf는 여러 도구가 모인 큰 디렉터리에 들어 있는데, 저는 qf만 컴파일하고 싶었습니다. 그래서 이렇게 qf만 컴파일했습니다.
make qf
기본적으로 빌드하려는 파일의 출력 파일 이름을 알고 있다면(또는 추측할 수 있다면), make $FILENAME을 실행해서 make에게 그 파일만 빌드하라고 시킬 수 있습니다
팁: Makefile이 없어도 됩니다
저는 가끔 의존성이 없는 5줄짜리 C 프로그램을 작성하는데, blah.c라는 파일이 있다면 Makefile을 만들지 않고도 다음과 같이 바로 컴파일할 수 있다는 걸 최근에 알게 되었습니다.
make blah
이 명령은 자동으로 cc -o blah blah.c로 확장되어 타이핑을 조금 줄여줍니다. 이걸 기억할 수 있을지는 모르겠지만(그냥 계속 gcc -o blah blah.c라고 입력할지도 모릅니다), 재미있는 요령인 것 같습니다.
팁: 다른 패키징 시스템이 같은 C 프로그램을 어떻게 빌드했는지 살펴보기
C 프로그램 빌드에 어려움을 겪고 있다면, 다른 사람들도 같은 문제를 겪었을지 모릅니다! 모든 리눅스 배포판은 빌드하는 모든 패키지에 대한 빌드 파일을 가지고 있으므로, 해당 배포판의 패키지를 직접 설치할 수 없더라도 패키지 빌드 방법에 대한 힌트를 얻을 수 있습니다. 이 사실을 깨달은 것(친구 Dave 덕분입니다)은 저에게 큰 깨달음이었습니다.
예를 들어, 이 줄은 paperjam용 nix 패키지에서 가져온 것인데 이렇게 되어 있습니다.
env.NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-liconv";
이는 기본적으로 “맥에서 빌드하려면 링커 플래그 -liconv를 넘기라”는 뜻이므로, 빌드에 활용할 수 있는 단서가 됩니다.
같은 파일에는 env.NIX_CFLAGS_COMPILE = "-DPOINTERHOLDER_TRANSITION=1";라는 내용도 있습니다. 이게 무슨 뜻인지는 잘 모르겠지만, paperjam 패키지를 빌드하려고 하면 실제로 PointerHolder라는 것에 대한 오류가 발생하므로, 아마 “PointerHolder 전환”과 관련이 있는 것 같습니다.
5단계: 바이너리 설치하기
프로그램을 컴파일하는 데 성공했다면 아마 어딘가에 설치하고 싶을 것입니다! 일부 Makefile에는 install 타깃이 있어서 make install로 시스템에 도구를 설치할 수 있습니다. 저는 이게 항상 조금 무섭습니다(파일을 어디에 넣는 걸까? 나중에 삭제하고 싶으면 어떻게 하지?), 그래서 꽤 단순한 프로그램을 컴파일할 때는 종종 이렇게 바이너리를 직접 복사해서 설치합니다.
cp qf ~/bin
6단계: 직접 패키지를 만들어 보는 것도 방법입니다!
이 모든 방법을 알게 된 뒤에는 새로 얻은 make 지식을 활용해 Homebrew에 paperjam 패키지를 기여할 수도 있겠다는 생각이 들었습니다! 그러면 앞으로는 다른 시스템에서도 그냥 brew install paperjam으로 설치할 수 있을 테니까요.
좋은 점은 서로 다른 패키징 시스템들의 세부 사항이 다르더라도, 근본적으로는 모두 C 컴파일러와 링커를 사용한다는 것입니다.
C 프로그래머가 아니더라도 C를 조금 이해해 두면 유용할 수 있습니다
저는 이 모든 과정이, 살면서 제대로 된 C 프로그램을 쓸 계획이 없더라도 C 프로그램이 어떻게 동작하는지에 대한 기초(예를 들어 “헤더 파일이 있다”는 것)를 이해해 두는 것이 얼마나 유용할 수 있는지를 보여주는 흥미로운 사례라고 생각합니다.
컴파일러와 링커 플래그에 대해 아직 완전히 자신 있는 건 아니고, autotools에 대해서도 “./configure를 실행해 Makefile을 생성한다”는 것 이상은 앞으로도 배울 생각이 없지만, 그래도 C/C++ 프로그램을 스스로 컴파일할 수 있는 능력이 생기니 기분이 좋습니다.
이 글에서 빠뜨린 두 가지는 다음과 같습니다.
LD_LIBRARY_PATH / DYLD_LIBRARY_PATH(동적 링커에게 런타임에 동적 링크된 파일을 어디서 찾을지 알려줄 때 사용합니다) — 마지막으로LD_LIBRARY_PATH문제를 겪은 게 언제인지 기억나지 않고 예시도 찾지 못해서 제외했습니다.pkg-config— 중요하다고 생각하지만 아직 이해하지 못했습니다
글을 무작위로 읽기