크로스컴파일 sysroot 헤더 없음, 어떻게 채우나

크로스컴파일에서 **xxx.h: No such file or directory**가 나면, 컴파일러가 호스트 /usr/include를 보거나 sysroot 안에 타깃 헤더가 빠진 경우가 많습니다. GCC Directory Options--sysroot, pkg-config 크로스 컴파일의 **PKG_CONFIG_SYSROOT_DIR**로 경로를 맞추고, 보드(또는 SDK) 루트에서 **헤더·라이브러리·.pc**만 채워 넣습니다. CMake toolchain·CMAKE_SYSROOT 배선은 aarch64-cmake-toolchain에만 짧게 맡기고, 여기서는 누락 헤더를 채우는 쪽만 다룹니다. 제휴·보드 실화·가격은 없습니다.

include 경로가 꼬이는 신호는?

한 줄 답: 파일이 없다호스트 경로를 보고 있다를 가릅니다. --sysroot가 먹으면 표준 include·lib가 dir/usr/include, dir/usr/lib로 옮겨지고, -I/-L= 또는 $SYSROOT로 시작하면 그 접두가 sysroot로 치환됩니다(GCC Directory Options).

대표 신호:

신호보통 의미
fatal error: foo.h: No such file or directorysysroot에 헤더 없음, 또는 --sysroot/-isysroot가 안 붙음
-E -v / -v에 호스트 /usr/include만 보임크로스 드라이버에 sysroot가 안 전달됨
헤더는 찾았는데 ABI·아키텍처가 어긋남호스트 헤더/라이브러리를 집음 — “없음”보다 위험한 성공
pkg-config --cflags/usr/include/...만 반환.pc는 읽었지만 sysroot prefix가 안 붙음

확인 예(툴체인 prefix는 환경에 맞게):

# include 탐색 순서 덤프 (전처리만)
aarch64-linux-gnu-gcc --sysroot=/opt/target-sysroot -E -v -xc /dev/null

# 특정 헤더가 sysroot 어디에 있는지
find /opt/target-sysroot -name 'foo.h' 2>/dev/null

GCC 요지:

  1. --sysroot=dir: 헤더·라이브러리의 논리 루트. 원래 /usr/include를 보면 dir/usr/include를 봅니다.
  2. -isysroot: 헤더만 다른 루트를 줄 때(라이브러리는 --sysroot 유지). 둘 다 쓰면 역할이 갈립니다.
  3. -I=/usr/include/foo 또는 -I$SYSROOT/usr/include/foo: =/$SYSROOT가 sysroot 접두로 바뀝니다. 절대 경로 -I/usr/...는 호스트를 직접 찌를 수 있습니다.

빌드 로그에 -I/usr/includesysroot 없이 반복되면, 플래그 생성기(pkg-config·CMake find)가 크로스 설정을 무시한 신호입니다. CMake로 --sysroot를 자동 부착하는 쪽은 aarch64-cmake-toolchainCMAKE_SYSROOT만 보면 됩니다.

pkg-config는 어떻게 맞추나?

한 줄 답: **PKG_CONFIG_SYSROOT_DIR**에 sysroot를 두고, **PKG_CONFIG_LIBDIR**로 타깃 .pc 검색 경로만 가리키며, 호스트를 섞지 않으려면 PKG_CONFIG_PATH를 비웁니다. sysroot 값은 -I/-L 쪽에 끼워지고, --variable로 꺼낸 경로에는 기본적으로 안 붙습니다(autotools.info pkg-config cross-compiling, pkgconf(1)).

최소 환경 예:

export SYSROOT=/opt/target-sysroot
export PKG_CONFIG_SYSROOT_DIR=$SYSROOT
export PKG_CONFIG_LIBDIR=$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig
# multiarch 트리면 예: $SYSROOT/usr/lib/aarch64-linux-gnu/pkgconfig 도 추가
export PKG_CONFIG_PATH=

검증:

pkg-config --exists libfoo && echo ok
pkg-config --cflags libfoo
pkg-config --libs libfoo

--cflags/--libs에 **$SYSROOT가 앞에 붙은 -I/-L**이 보이면 prefix 주입이 동작 중인 것입니다. 호스트 /usr/lib/pkgconfig.pc가 그대로 나오면 PKG_CONFIG_LIBDIR·PATH부터 다시 잡습니다.

실무 메모:

  1. 래퍼: 호스트용 pkg-config와 타깃용 설정을 섞지 않으려면 ${CHOST}-pkg-config 래퍼에서 위 변수를 export한 뒤 본체를 호출하는 패턴이 문서에 나옵니다.
  2. --variable=prefix 등: sysroot가 붙을 수 있습니다. 크로스 빌드에서 그 값을 컴파일 플래그처럼 쓰면 호스트 경로가 새어 나갑니다.
  3. .pc 파일 자체: prefix=/usr처럼 타깃 레이아웃을 유지하고, 호스트 절대 경로를 .pc에 하드코딩하지 않습니다. 검색은 LIBDIR, 경로 접두는 SYSROOT_DIR이 맡습니다.
  4. Meson/CMake: 크로스 파일·toolchain에서 **타깃 pkg-config 바이너리(또는 래퍼)**를 가리키게 하는 편이, 전역 env만 믿는 것보다 안전합니다. CMake 쪽 find 모드는 위 toolchain 글의 CMAKE_FIND_ROOT_PATH_MODE_*를 따릅니다.

보드 이미지에서 무엇을 복사하나?

한 줄 답: sysroot는 타깃 루트 파일시스템의 개발에 필요한 부분입니다. 런타임 루트 전체를 통째로 옮기기보다, 레이아웃을 유지한 채 헤더·링크용 라이브러리·pkg-config를 채웁니다. SDK/sysroot를 배포판이 이미 주면 그걸 쓰고, 보드 루트(또는 루트fs 이미지)에서 채울 때도 같은 트리 규칙을 따릅니다.

보통 복사·동기화 대상(경로는 sysroot 기준):

경로역할
usr/include/C/C++ 헤더(및 패키지별 하위 디렉터리)
lib/, usr/lib/공유 라이브러리·링커 스크립트·.so 심볼릭 링크
usr/lib/*/pkgconfig/, usr/share/pkgconfig/.pc (아키텍처 트리면 multiarch libdir 포함)
(필요 시) usr/lib/*.a, 정적 링크용 아카이브정적 링크할 때만
(필요 시) lib64 / usr/lib/<triplet>/타깃 배포가 multiarch·lib64를 쓸 때 그대로

의도적으로 빼도 되는 것(빌드 sysroot 목적):

  • /home, /var/log, 캐시·저널, 컨테이너 런타임 상태
  • 디바이스 노드 전체(/dev), 불필요한 /proc·/sys 마운트 내용
  • -dev/-devel 없이 런타임 .so만 있는 이미지에서 헤더를 기대하는 것 — 헤더는 개발 패키지·SDK·소스 설치 트리가 있어야 생깁니다

동기화 예(호스트에서, 권한·심볼릭 링크 유지):

SYSROOT=/opt/target-sysroot
ROOTFS=/mnt/target-rootfs   # 마운트한 루트fs 또는 보드 NFS 루트

mkdir -p "$SYSROOT"
rsync -a --delete \
  --include='usr/include/***' \
  --include='lib/***' \
  --include='usr/lib/***' \
  --include='usr/share/pkgconfig/***' \
  --include='usr/' --include='lib/' --include='usr/share/' \
  --exclude='*' \
  "$ROOTFS/" "$SYSROOT/"

필터는 배포 레이아웃에 맞게 조정합니다. 핵심은 **타깃에서 /usr/include/foo.h였으면 sysroot에서도 …/usr/include/foo.h**가 되게 하는 것입니다. --sysroot는 그 논리 루트만 바꿉니다.

채운 뒤:

test -f "$SYSROOT/usr/include/stdio.h" && echo libc-headers-ok
# 의존 패키지 헤더·.pc가 실제 쓰는지 한 번 더
pkg-config --exists libfoo && pkg-config --cflags libfoo
aarch64-linux-gnu-gcc --sysroot="$SYSROOT" -E -xc - <<'EOF'
#include <foo.h>
EOF

Yocto/Buildroot 등 이미 sysroot·SDK를 내보내는 빌드 시스템은 보드에서 수동 복사보다 그 산출물을 쓰는 편이 문서·버전과 맞습니다. 이 글의 복사는 “이미지에 헤더가 있는데 호스트 sysroot만 비어 있을 때”의 최소 절차입니다.

자주 묻는 질문

Q. 헤더만 복사하고 .so는 안 넣어도 되나요?
전처리·문법 검사만이면 헤더만으로도 충분할 수 있으나, 링크 단계에서 .so/링커 스크립트·종종 .pc-l이 필요합니다. include 오류를 고친 다음 바로 undefined reference가 나면 라이브러리·심볼릭 링크 트리를 의심합니다.

Q. -I에 호스트 경로를 직접 주면 안 되나요?
당장 컴파일은 될 수 있어도 타깃과 다른 헤더를 먹습니다. GCC가 안내한 =/…·$SYSROOT 형태나 --sysroot 표준 경로를 쓰는 편이 안전합니다.

Q. PKG_CONFIG_SYSROOT_DIR만 켜면 충분한가요?
충분하지 않은 경우가 많습니다. 어느 .pc를 읽을지(LIBDIR/PATH)와 읽은 경로에 접두를 붙일지(SYSROOT_DIR)는 별개입니다. 호스트 .pc를 읽으면 접두만 붙어 이상한 경로가 됩니다.

Q. CMake toolchain은 여기서 다시 짜나요?
아닙니다. CMAKE_SYSROOT·find 모드·컴파일러 지정은 aarch64-cmake-toolchain만 보고, 여기서는 그 sysroot 디렉터리를 무엇으로 채울지에 집중합니다.

정리

  1. 신호: “헤더 없음” vs “호스트 include를 봄” — gcc --sysroot=… -E -vfind $SYSROOT -name '*.h'로 가릅니다.
  2. pkg-config: PKG_CONFIG_SYSROOT_DIR + 타깃 PKG_CONFIG_LIBDIR, PKG_CONFIG_PATH=--cflags에 sysroot가 붙는지 확인합니다.
  3. 채우기: 보드/루트fs·SDK에서 usr/include, lib/usr/lib, pkgconfig레이아웃 유지로 복사하고, 런타임 전용 이미지에 -dev 헤더가 없는지는 먼저 확인합니다.
  4. CMake 배선은 재탕하지 않고 aarch64-cmake-toolchain으로만 연결합니다.

근거: GCC Directory Options (--sysroot, -isysroot), pkg-config cross-compiling, pkgconf(1) PKG_CONFIG_SYSROOT_DIR.