C++ 크로스 플랫폼 빌드 완벽 가이드 | CMake 툴체인·CPack·ABI 안정성·프로덕션 패턴
이 글의 핵심
C++ 크로스 플랫폼 빌드 입니다. CMake 툴체인, CPack 패키징, ABI 안정성, 프로덕션 배포 전략을 실전 예제로 다룹니다. 크로스 플랫폼 C++ 프로젝트에서는 플랫폼별 경로·라이브러리·컴파일러·ABI가 달라 빌드 실패, 런타임 크래시, 패키징 불일치가 빈번합니다. 비유하면 "각 나라마다 전압·플러그·규격이 다른 것"처럼,.
들어가며: “팀원마다 OS가 다른데 빌드가 매번 깨져요"
"Windows에서 빌드한 DLL을 Linux 서버에서 로드하려다 크래시했어요”
크로스 플랫폼 C++ 프로젝트에서는 플랫폼별 경로·라이브러리·컴파일러·ABI가 달라 빌드 실패, 런타임 크래시, 패키징 불일치가 빈번합니다. 비유하면 “각 나라마다 전압·플러그·규격이 다른 것”처럼, OS별로 빌드·실행·배포 환경이 다르기 때문에 툴체인·조건부 컴파일·ABI 안정화가 필수입니다.
문제의 핵심:
- Windows:
\경로,LoadLibrary,ws2_32.dll, MSVC/MinGW ABI 불일치 - Linux:
/경로,dlopen,pthread, GCC/Clang, 심볼 버전 관리 - macOS:
dlopen,pthread, Clang, 코드 서명·Xcode 규칙 - 모바일: iOS/Android 각각 별도 툴체인·SDK·ABI
이 글에서 다루는 것:
- 문제 시나리오: 실무에서 겪는 크로스 플랫폼 빌드 고통 8가지
- 완전한 CMake 툴체인 파일: Linux·Windows·MinGW·iOS·Android
- CPack 패키징: DEB·RPM·NSIS·DMG·ZIP
- ABI 안정성: PIMPL·extern C·심볼 버전 관리
- 자주 발생하는 에러와 해결법
- 모범 사례와 프로덕션 패턴
요구 환경: C++17 이상, CMake 3.16+
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오: 크로스 플랫폼 빌드가 깨지는 순간
- 기본 개념: 플랫폼 감지·툴체인 흐름
- 완전한 CMake 툴체인 파일
- CPack 크로스 플랫폼 패키징
- ABI 안정성: PIMPL·extern C·심볼 버전
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- 정리
1. 문제 시나리오: 크로스 플랫폼 빌드가 깨지는 순간
시나리오 1: 경로 구분자로 인한 파일 열기 실패
문제: Windows에서 path/to/file.txt 같은 경로로 파일을 열면 std::ifstream이 실패합니다. Windows는 \를 구분자로 쓰지만, Linux는 /를 씁니다. 하드코딩된 경로로는 한 플랫폼에서만 동작합니다.
해결: std::filesystem::path를 사용하면 OS가 자동으로 올바른 구분자를 사용합니다.
시나리오 2: Windows에서만 “undefined reference to pthread”
문제: Linux에서 -lpthread로 링크한 코드가 Windows에서 빌드됩니다. Windows에는 pthread가 없고, MSVC는 스레드를 기본 지원합니다. CMake find_package(Threads)로 플랫폼별 스레드 라이브러리를 자동 링크합니다.
시나리오 3: 동적 라이브러리 확장자 불일치
문제: 플러그인을 로드할 때 plugin.so로만 찾습니다. Windows에서는 plugin.dll, macOS에서는 plugin.dylib이 필요합니다.
해결: CMAKE_SHARED_LIBRARY_SUFFIX 또는 플랫폼별 매크로로 확장자를 분기합니다.
시나리오 4: 모바일 크로스 컴파일 실패
문제: 데스크톱용 CMake로 iOS·Android 앱을 빌드하려 합니다. arm64·armv7 아키텍처, iOS SDK 경로, Android NDK 경로가 설정되지 않아 빌드가 실패합니다.
해결: CMake 툴체인 파일로 iOS/Android 전용 컴파일러·SDK를 지정합니다.
시나리오 5: MSVC로 빌드한 DLL을 MinGW 앱에서 로드 시 크래시
문제: Windows에서 MSVC로 빌드한 DLL을 MinGW로 빌드한 앱에서 로드합니다. C++ ABI·표준 라이브러리(libstdc++ vs MSVCRT)가 달라 런타임 크래시가 발생합니다.
해결: C ABI로 경계를 만들거나, 같은 툴체인으로 빌드합니다. 플러그인·DLL은 extern "C"로 내보냅니다.
시나리오 6: 패키징 시 의존성 누락
문제: CPack으로 DEB를 만들었는데, 설치 후 실행 시 “libfoo.so.1: cannot open shared object file” 에러가 납니다. 런타임 의존성이 패키지에 포함되지 않았습니다.
해결: CPACK_INSTALL_CMAKE_PROJECTS, CPACK_DEBIAN_PACKAGE_DEPENDS 등으로 의존성을 명시하고, install(RUNTIME_DEPENDENCY_SET)으로 동적 라이브러리를 수집합니다.
시나리오 7: Linux에서 심볼 버전 충돌
문제: libstdc++를 업그레이드한 후, 구버전으로 빌드한 플러그인이 “version `GLIBCXX_3.4.30’ not found”로 로드 실패합니다.
해결: 플러그인에 심볼 버전 스크립트를 적용해 필요한 심볼만 내보내고, 배포 시 최소 glibc/libstdc++ 버전을 명시합니다.
시나리오 8: CI에서 플랫폼별 빌드 매트릭스 실패
문제: 로컬 macOS에서는 빌드가 되는데, GitHub Actions의 windows-latest에서만 실패합니다. MSVC 경로·vcpkg triplet·코드 페이지 설정이 다릅니다.
해결: 툴체인 파일을 표준화하고, CI 스크립트에서 플랫폼별로 동일한 CMake 옵션을 사용합니다.
2. 기본 개념: 플랫폼 감지·툴체인 흐름
플랫폼 감지 매크로
// platform_detect.hpp — 플랫폼별 매크로
#if defined(_WIN32) || defined(_WIN64)
#define PLATFORM_WINDOWS 1
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
#define PLATFORM_IOS 1
#else
#define PLATFORM_MACOS 1
#endif
#elif defined(__linux__)
#if defined(__ANDROID__)
#define PLATFORM_ANDROID 1
#else
#define PLATFORM_LINUX 1
#endif
#else
#define PLATFORM_UNKNOWN 1
#endif
CMake 플랫폼 변수
# CMake에서 플랫폼 감지
if(WIN32)
message(STATUS "Building for Windows")
elseif(APPLE)
message(STATUS "Building for macOS or iOS")
elseif(UNIX AND NOT APPLE)
message(STATUS "Building for Linux")
endif()
message(STATUS "CMAKE_SYSTEM_PROCESSOR: ${CMAKE_SYSTEM_PROCESSOR}")
# x86_64, AMD64, ARM64, aarch64 등
크로스 플랫폼 빌드 흐름
flowchart TB
subgraph source[소스 코드]
S1[공통 C++ 코드]
S2[플랫폼별 #ifdef]
S3[추상화 레이어]
end
subgraph cmake[CMake]
C1[CMakeLists.txt]
C2[툴체인 파일]
C3[플랫폼별 옵션]
end
subgraph targets[타겟 플랫폼]
T1[Windows .exe/.dll]
T2[Linux .so]
T3[macOS .dylib]
T4[iOS/Android]
end
S1 --> C1
S2 --> C1
S3 --> C1
C1 --> C2
C1 --> C3
C2 --> T1
C2 --> T2
C2 --> T3
C2 --> T4
3. 완전한 CMake 툴체인 파일
3.1 프로젝트 구조
project/
├── toolchains/
│ ├── linux-x64.cmake
│ ├── mingw64.cmake
│ ├── ios.toolchain.cmake
│ └── android-arm64.cmake
├── CMakeLists.txt
└── src/
3.2 Linux x64 툴체인 (크로스 컴파일용)
# toolchains/linux-x64.cmake
# 사용: cmake -B build -DCMAKE_TOOLCHAIN_FILE=toolchains/linux-x64.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# 크로스 컴파일러 지정 (선택)
# set(CMAKE_C_COMPILER /usr/bin/x86_64-linux-gnu-gcc)
# set(CMAKE_CXX_COMPILER /usr/bin/x86_64-linux-gnu-g++)
# sysroot (크로스 컴파일 시)
# set(CMAKE_SYSROOT /path/to/sysroot)
# 빌드 머신 도구는 호스트 것 사용
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
3.3 MinGW64 툴체인 (Linux/macOS에서 Windows 빌드)
# toolchains/mingw64.cmake
# 사용: cmake -B build -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw64.cmake
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# MinGW 경로 (환경에 맞게 수정)
set(MINGW_PREFIX /usr/local/x86_64-w64-mingw32
CACHE PATH "MinGW-w64 installation prefix")
set(CMAKE_C_COMPILER ${MINGW_PREFIX}/bin/x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER ${MINGW_PREFIX}/bin/x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER ${MINGW_PREFIX}/bin/x86_64-w64-mingw32-windres)
set(CMAKE_FIND_ROOT_PATH ${MINGW_PREFIX})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# 정적 링크 옵션 (선택)
# set(CMAKE_EXE_LINKER_FLAGS "-static-libgcc -static-libstdc++")
3.4 iOS 툴체인
# toolchains/ios.toolchain.cmake
# 사용: cmake -B build-ios -DCMAKE_TOOLCHAIN_FILE=toolchains/ios.toolchain.cmake
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_SYSROOT iphoneos CACHE STRING "iOS SDK")
set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "iOS architectures")
set(CMAKE_OSX_DEPLOYMENT_TARGET "14.0" CACHE STRING "Minimum iOS version")
# 코드 서명 (개발 빌드)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "" CACHE STRING "")
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO" CACHE STRING "")
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO" CACHE STRING "")
# 빌드 타입
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "")
endif()
3.5 Android NDK 툴체인
# toolchains/android-arm64.cmake
# 사용: cmake -B build-android \
# -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
# -DANDROID_ABI=arm64-v8a \
# -DANDROID_PLATFORM=android-21
# (Android NDK는 자체 툴체인 제공 — android.toolchain.cmake 사용)
# 커스텀 옵션 예시
set(ANDROID_ABI arm64-v8a CACHE STRING "")
set(ANDROID_PLATFORM android-21 CACHE STRING "")
set(ANDROID_STL c++_shared CACHE STRING "")
3.6 메인 CMakeLists.txt — 툴체인 통합
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(CrossPlatformApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
# 플랫폼별 라이브러리
if(WIN32)
set(PLATFORM_LIBS ws2_32)
elseif(UNIX AND NOT APPLE)
find_package(Threads REQUIRED)
find_library(DL_LIBRARY dl)
set(PLATFORM_LIBS Threads::Threads ${DL_LIBRARY})
elseif(APPLE)
find_package(Threads REQUIRED)
set(PLATFORM_LIBS Threads::Threads)
endif()
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE ${PLATFORM_LIBS})
# 플랫폼별 컴파일 정의
if(WIN32)
target_compile_definitions(app PRIVATE PLATFORM_WINDOWS=1)
elseif(APPLE)
target_compile_definitions(app PRIVATE PLATFORM_APPLE=1)
else()
target_compile_definitions(app PRIVATE PLATFORM_LINUX=1)
endif()
# 공유 라이브러리 (플러그인)
add_library(plugin SHARED src/plugin.cpp)
target_link_libraries(plugin PRIVATE ${PLATFORM_LIBS})
set_target_properties(plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON
WINDOWS_EXPORT_ALL_SYMBOLS OFF
)
if(NOT WIN32)
target_compile_definitions(plugin PRIVATE PLUGIN_EXPORT=__attribute__\(\(visibility\(\"default\"\)\)\))
endif()
4. CPack 크로스 플랫폼 패키징
4.1 CPack 기본 설정
# CMakeLists.txt 하단에 추가
include(InstallRequiredSystemLibraries)
set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Cross-platform C++ Application")
set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md")
# 설치 규칙
install(TARGETS app RUNTIME DESTINATION bin)
install(TARGETS plugin LIBRARY DESTINATION lib RUNTIME DESTINATION bin)
# 플랫폼별 제너레이터
if(WIN32)
set(CPACK_GENERATOR "NSIS;ZIP")
set(CPACK_NSIS_MODIFY_PATH ON)
set(CPACK_NSIS_INSTALLER_MUI_ICON "${CMAKE_SOURCE_DIR}/icon.ico")
elseif(APPLE)
set(CPACK_GENERATOR "DragNDrop;TGZ")
set(CPACK_DMG_DS_STORE_SET_OWNER "${CMAKE_INSTALL_PREFIX}")
else()
set(CPACK_GENERATOR "DEB;RPM;TGZ")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "[email protected]")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.27)")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
endif()
include(CPack)
4.2 DEB 패키지 상세 설정
# DEB 전용 옵션
set(CPACK_DEBIAN_PACKAGE_NAME "myapp")
set(CPACK_DEBIAN_PACKAGE_VERSION "1.0.0")
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.27), libstdc++6 (>= 9)")
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
4.3 NSIS (Windows 인스톨러) 설정
set(CPACK_NSIS_PACKAGE_NAME "MyApp")
set(CPACK_NSIS_DISPLAY_NAME "My Application")
set(CPACK_NSIS_HELP_LINK "https://example.com/support")
set(CPACK_NSIS_URL_INFO_ABOUT "https://example.com")
set(CPACK_NSIS_CONTACT "[email protected]")
set(CPACK_NSIS_MODIFY_PATH ON)
4.4 런타임 의존성 수집 (CMake 3.21+)
# 동적 라이브러리 의존성 자동 수집
include(GNUInstallDirs)
install(TARGETS app plugin
RUNTIME
COMPONENT runtime
DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY
COMPONENT runtime
DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(RUNTIME_DEPENDENCY_SET app_deps
TARGETS app
PRE_INCLUDE_REGEXES ".*"
PRE_EXCLUDE_REGEXES ".*system.*"
POST_INCLUDE_REGEXES ".*"
POST_EXCLUDE_REGEXES ""
DIRECTORIES ${CMAKE_SOURCE_DIR}/lib
)
4.5 CPack 빌드 명령
# 빌드 후 패키징
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
cd build && cpack -G DEB # 또는 NSIS, ZIP, TGZ 등
5. ABI 안정성: PIMPL·extern C·심볼 버전
5.1 PIMPL로 ABI 고정
PIMPL(Pointer to Implementation)은 공개 클래스가 구현체 포인터만 갖고, 구현체 정의는 .cpp에만 두어 공개 클래스 크기를 고정합니다. 내부 구현 변경 시에도 ABI가 유지됩니다.
// widget.h — 공개 헤더 (ABI 고정)
#pragma once
#include <memory>
#include <string>
class WidgetImpl;
class Widget {
public:
Widget();
~Widget();
Widget(const Widget&);
Widget& operator=(const Widget&);
Widget(Widget&&) noexcept = default;
Widget& operator=(Widget&&) noexcept = default;
void setTitle(const std::string& title);
[[nodiscard]] std::string getTitle() const;
private:
std::unique_ptr<WidgetImpl> pImpl_; // 크기 고정: 8바이트
};
// widget.cpp — 구현 (내부 변경 가능)
#include "widget.h"
#include <unordered_map>
class WidgetImpl {
public:
std::string title;
std::unordered_map<std::string, size_t> cache; // v2에서 추가해도 ABI 유지
};
Widget::Widget() : pImpl_(std::make_unique<WidgetImpl>()) {}
Widget::~Widget() = default;
Widget::Widget(const Widget& other)
: pImpl_(std::make_unique<WidgetImpl>(*other.pImpl_)) {}
Widget& Widget::operator=(const Widget& other) {
if (this != &other) *pImpl_ = *other.pImpl_;
return *this;
}
void Widget::setTitle(const std::string& title) { pImpl_->title = title; }
std::string Widget::getTitle() const { return pImpl_->title; }
5.2 extern “C” 플러그인 API
C++ name mangling은 컴파일러마다 다릅니다. extern “C”로 내보내면 심볼 이름이 고정되어, 다른 컴파일러로 빌드한 바이너리와도 링크할 수 있습니다.
// plugin_api.h — C 인터페이스 (ABI 최대 안정)
#pragma once
#include <cstdint>
#include <cstddef>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct PluginHandle* PluginHandlePtr;
PluginHandlePtr plugin_create(const char* config);
void plugin_destroy(PluginHandlePtr handle);
int plugin_process(PluginHandlePtr handle, const void* input, size_t input_size,
void* output, size_t output_size);
#ifdef __cplusplus
}
#endif
// plugin_impl.cpp — C++ 래퍼가 C API 구현
#include "plugin_api.h"
#include <string>
#include <cstring>
struct PluginImpl {
std::string config;
};
extern "C" {
#if defined(_WIN32)
#define PLUGIN_EXPORT __declspec(dllexport)
#else
#define PLUGIN_EXPORT __attribute__((visibility("default")))
#endif
PLUGIN_EXPORT PluginHandlePtr plugin_create(const char* config) {
auto* p = new PluginImpl;
if (config) p->config = config;
return p;
}
PLUGIN_EXPORT void plugin_destroy(PluginHandlePtr handle) {
delete static_cast<PluginImpl*>(handle);
}
PLUGIN_EXPORT int plugin_process(PluginHandlePtr handle,
const void* input, size_t input_size, void* output, size_t output_size) {
(void)handle;
(void)input;
(void)input_size;
(void)output;
(void)output_size;
return 0;
}
}
5.3 Linux 심볼 버전 관리
플러그인·라이브러리에서 필요한 심볼만 내보내고, 버전을 부여해 ABI 호환성을 관리합니다.
# plugin.ver — 심볼 버전 스크립트
PLUGIN_1.0 {
global:
plugin_create;
plugin_destroy;
plugin_process;
local: *;
};
PLUGIN_1.1 {
global:
plugin_init;
} PLUGIN_1.0;
# CMakeLists.txt — Linux에서 심볼 버전 적용
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(plugin PRIVATE
"LINKER:--version-script=${CMAKE_CURRENT_SOURCE_DIR}/plugin.ver"
)
endif()
# 직접 링크 시
g++ -shared -Wl,--version-script=plugin.ver -o libplugin.so plugin.cpp
5.4 ABI 안정성 비교
| 기법 | ABI 안정성 | 사용 편의성 | 적용 시점 |
|---|---|---|---|
| PIMPL | 높음 | 중간 | 공개 C++ 클래스 |
| extern “C” | 최고 | 낮음 | 플러그인·DLL 경계 |
| 심볼 버전 | 중간 | 낮음 | Linux .so 버전 관리 |
6. 자주 발생하는 에러와 해결법
에러 1: “fatal error: ‘unistd.h’ file not found” (Windows)
원인: unistd.h는 POSIX 전용 헤더입니다. Windows(MSVC)에는 없습니다.
해결:
// ❌ 잘못된 코드
#include <unistd.h>
// ✅ 올바른 코드
#if defined(_WIN32)
#include <io.h>
#include <process.h>
#define close _close
#else
#include <unistd.h>
#endif
에러 2: “undefined reference to dlopen” (Linux)
원인: dlopen는 libdl에 있습니다. 링크하지 않으면 에러가 납니다.
해결:
# CMakeLists.txt
if(UNIX AND NOT APPLE)
target_link_libraries(app PRIVATE dl)
endif()
에러 3: “DLL not found” 또는 “dyld: Library not loaded” (런타임)
원인: 동적 라이브러리 경로가 PATH(Windows) 또는 LD_LIBRARY_PATH(Linux)·DYLD_LIBRARY_PATH(macOS)에 없습니다.
해결:
# Linux
export LD_LIBRARY_PATH=/path/to/libs:$LD_LIBRARY_PATH
./app
# macOS
export DYLD_LIBRARY_PATH=/path/to/libs:$DYLD_LIBRARY_PATH
./app
# Windows: 실행 파일와 같은 디렉터리에 DLL을 두거나 PATH에 추가
에러 4: “symbol not found” (macOS)
원인: macOS는 기본적으로 모든 심볼이 숨겨져 있습니다. 내보내려면 __attribute__((visibility("default")))가 필요합니다.
해결:
#if defined(__APPLE__)
#define EXPORT_API __attribute__((visibility("default")))
#elif defined(_WIN32)
#define EXPORT_API __declspec(dllexport)
#else
#define EXPORT_API __attribute__((visibility("default")))
#endif
extern "C" EXPORT_API void my_exported_function();
에러 5: “LNK2019: unresolved external symbol” (Windows)
원인: __declspec(dllimport)/dllexport가 누락되었습니다.
해결:
// mylib.h
#if defined(_WIN32)
#ifdef MYLIB_EXPORTS
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
#else
#define MYLIB_API
#endif
MYLIB_API void my_function();
add_library(mylib SHARED mylib.cpp)
target_compile_definitions(mylib PRIVATE MYLIB_EXPORTS)
에러 6: MinGW에서 “undefined reference to WinMain”
원인: 콘솔 앱인데 add_executable(app WIN32 ...)로 GUI 앱으로 빌드했습니다.
해결:
# 콘솔 앱
add_executable(app main.cpp)
# WIN32 제거
에러 7: “version `GLIBCXX_3.4.30’ not found”
원인: 구버전 libstdc++로 빌드한 바이너리를 새 환경에서 실행하려 할 때, 또는 그 반대입니다.
해결: 동일한 툴체인으로 빌드하거나, -static-libstdc++로 정적 링크합니다. 배포 시 최소 glibc/libstdc++ 버전을 명시합니다.
에러 8: CPack DEB 빌드 시 “dpkg-shlibdeps: error”
원인: dpkg-shlibdeps가 패키지된 라이브러리의 의존성을 분석할 수 없습니다. 경로가 잘못되었거나 라이브러리가 누락되었습니다.
해결:
# CPACK_DEBIAN_PACKAGE_SHLIBDEPS를 OFF로 (권장하지 않음)
# 또는 install(RUNTIME_DEPENDENCY_SET)으로 의존성 수집
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON)
에러 9: endianness·정렬 문제
원인: ARM과 x86의 바이트 순서가 다를 수 있습니다.
해결:
#include <cstdint>
inline uint32_t swapEndian(uint32_t x) {
return ((x >> 24) & 0xff) | ((x >> 8) & 0xff00) |
((x << 8) & 0xff0000) | ((x << 24) & 0xff000000);
}
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
#define TO_NETWORK_ORDER(x) (x)
#else
#define TO_NETWORK_ORDER(x) swapEndian(x)
#endif
7. 모범 사례
7.1 플랫폼별 코드 최소화
// ❌ 나쁜 예: 플랫폼별 코드가 산재
void doWork() {
#ifdef _WIN32
// Windows 전용 50줄
#else
// Unix 전용 50줄
#endif
}
// ✅ 좋은 예: 추상화 레이어로 분리
void doWork() {
platform::init();
platform::execute();
platform::cleanup();
}
7.2 std::filesystem 사용
// ❌ 나쁜 예
std::string path = "config" + std::string(1, '/') + "file.txt";
// ✅ 좋은 예
auto path = std::filesystem::path("config") / "file.txt";
7.3 고정 크기 정수 타입
// 플랫폼마다 long 크기가 다를 수 있음
#include <cstdint>
int64_t value; // 항상 8바이트
uint32_t count; // 항상 4바이트
7.4 CI에서 멀티 플랫폼 빌드
# .github/workflows/build.yml
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-cpp@v1
with:
compiler: ${{ matrix.os == 'windows-latest' && 'msvc' || 'gcc' }}
- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build
- name: Package
run: cd build && cpack -G ${{ matrix.os == 'ubuntu-latest' && 'DEB' || (matrix.os == 'windows-latest' && 'NSIS' || 'TGZ') }}
7.5 툴체인 파일 분리
project/
├── toolchains/
│ ├── linux-x64.cmake
│ ├── mingw64.cmake
│ ├── ios.toolchain.cmake
│ └── android-arm64.cmake
├── CMakeLists.txt
└── src/
7.6 ABI 경계에서 C 타입 사용
// ❌ API 경계에 std::string
extern "C" void process(std::string input);
// ✅ C 타입 사용
extern "C" void process(const char* input, size_t len);
8. 프로덕션 패턴
8.1 통합 빌드 스크립트
#!/bin/bash
# build-all.sh — 모든 플랫폼 빌드
set -e
BUILD_DIR=build
for p in linux windows macos; do
echo "Building for $p..."
case $p in
linux)
cmake -B $BUILD_DIR/linux -DCMAKE_BUILD_TYPE=Release
cmake --build $BUILD_DIR/linux
;;
windows)
cmake -B $BUILD_DIR/windows -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw64.cmake
cmake --build $BUILD_DIR/windows
;;
macos)
cmake -B $BUILD_DIR/macos -DCMAKE_BUILD_TYPE=Release
cmake --build $BUILD_DIR/macos
;;
esac
done
8.2 버전·플랫폼 정보 주입
if(EXISTS "${CMAKE_SOURCE_DIR}/.git")
execute_process(
COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
else()
set(GIT_HASH "unknown")
endif()
target_compile_definitions(app PRIVATE
GIT_HASH="${GIT_HASH}"
BUILD_PLATFORM="${CMAKE_SYSTEM_NAME}"
)
8.3 환경 변수 기반 툴체인 선택
#!/bin/bash
TARGET=${1:-native}
case $TARGET in
native)
cmake -B build
;;
windows)
cmake -B build -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw64.cmake
;;
android)
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a
;;
*)
echo "Unknown target: $TARGET"
exit 1
;;
esac
cmake --build build
8.4 플랫폼별 최적화 플래그
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
target_compile_options(app PRIVATE -march=native)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
target_compile_options(app PRIVATE -mcpu=native)
endif()
elseif(MSVC)
target_compile_options(app PRIVATE /arch:AVX2)
endif()
8.5 빌드 검증 시퀀스
sequenceDiagram
participant Dev as 개발자
participant CMake as CMake
participant CC as 컴파일러
participant CI as CI
Dev->>CMake: cmake -B build
CMake->>CMake: 플랫폼 감지
CMake->>CMake: 툴체인 설정
CMake-->>Dev: Makefile/솔루션 생성
Dev->>CC: cmake --build build
CC-->>Dev: 바이너리
Dev->>CI: push
CI->>CMake: 매트릭스 빌드 (Win/Linux/macOS)
CMake->>CC: 각 플랫폼 빌드
CI-->>Dev: 빌드 결과
9. 정리
| 항목 | 설명 |
|---|---|
| 플랫폼 감지 | _WIN32, __linux__, __APPLE__ 등 매크로 |
| 툴체인 | -DCMAKE_TOOLCHAIN_FILE로 크로스 컴파일 |
| CPack | DEB·RPM·NSIS·ZIP·TGZ 플랫폼별 패키징 |
| ABI 안정성 | PIMPL·extern C·심볼 버전 |
| 에러 | unistd.h·dlopen·DLL 경로·visibility |
핵심 원칙:
- 플랫폼별 코드 최소화: 추상화 레이어로 분리
- 표준 라이브러리 우선:
std::filesystem,std::thread - CMake 툴체인 활용: iOS·Android·MinGW 등
- ABI 경계: extern C 또는 PIMPL
- CI에서 멀티 플랫폼 빌드: 매트릭스 빌드로 검증
구현 체크리스트
-
std::filesystem으로 경로 처리 - 플랫폼별 매크로로
#ifdef분기 -
target_link_libraries에 플랫폼별 라이브러리 추가 - 동적 로딩 시 확장자(
.dll/.so/.dylib) 분기 -
extern "C"로 ABI 안정성 확보 (플러그인·DLL) - iOS·Android 툴체인 파일 준비
- CPack으로 DEB·NSIS·TGZ 패키징
- CI에서 Windows·Linux·macOS 빌드 검증
- Linux 심볼 버전 스크립트 (선택)
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 크로스 플랫폼 라이브러리 배포, 멀티 OS 지원 앱, 플러그인 시스템, CI/CD 파이프라인 구축 시 활용합니다. 본문의 툴체인·CPack·ABI 예제를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. CMake 기초(#04), CMake 고급(#17-1), 크로스 플랫폼(#55-4), ABI 안정성(#55-8)을 먼저 읽으면 이해가 쉽습니다.
Q. 프로덕션에서 주의할 점은?
A. CI에서 모든 지원 플랫폼을 매트릭스로 빌드 검증하고, ABI 경계는 extern C 또는 PIMPL로 고정하며, CPack으로 일관된 패키징을 적용합니다.
Q. vcpkg·Conan과 크로스 플랫폼 빌드를 함께 쓰려면?
A. vcpkg는 --triplet x64-windows 등으로 타겟을 지정합니다. Conan은 -s os=Linux -s arch=x86_64처럼 프로필로 지정합니다. CMake 툴체인과 triplet·프로필이 일치해야 합니다.
참고 자료
- CMake 공식 문서
- CPack 문서
- Itanium C++ ABI
- C++ 시리즈 #55-4: 크로스 플랫폼
- C++ 시리즈 #55-8: ABI 안정성
한 줄 요약: CMake 툴체인·CPack·ABI 안정성(PIMPL·extern C·심볼 버전)으로 Windows·Linux·macOS·모바일 크로스 플랫폼 빌드와 배포를 마스터할 수 있습니다.
관련 글
- C++ 크로스 플랫폼 기초 완벽 가이드 | 플랫폼 감지·std::filesystem
- C++ 크로스 플랫폼 테스트 완벽 가이드 | CI 매트릭스·Docker·엔디안·프로덕션 패턴 [실전]
- CMake 입문 | 수십 개 파일 컴파일할 때 필요한 빌드 자동화 (CMakeLists.txt 기초)
- C++ CMake Presets 완벽 가이드 | 멀티 플랫폼·vcpkg·Conan·CI/CD 통합