1회차에서 Yocto의 큰 그림을, 2회차에서 메타데이터 파일들을 봤으니, 이번엔 bitbake core-image-minimal 한 줄을 쳤을 때 빌드 시스템 내부에서 실제로 무슨 일이 벌어지는지를 따라가 볼 차례예요. 처음 빌드는 몇 시간씩 걸리는데 두 번째 빌드는 몇 분 만에 끝나는 마법 — 그 비밀인 sstate 캐시까지 오늘 한 번에 정리합니다. 이 내부 동작이 머리에 그려져야 빌드 에러 로그에서 "지금 어느 단계가 실패했는지"를 즉시 읽어낼 수 있어요.
실무에서 Yocto 빌드 에러를 만나면 로그에 반드시 do_compile, do_fetch 같은 이름이 찍혀 있어요. 이 이름들이 바로 오늘의 주인공인 태스크(task)입니다. BitBake는 레시피 하나를 빌드할 때 그 레시피를 통째로 실행하는 게 아니라, 소스 받기 → 압축 풀기 → 패치 → 설정 → 컴파일 → 설치 → 패키징이라는 잘게 쪼갠 태스크들의 사슬로 나눠 실행해요. 그래서 "어느 태스크에서 죽었는가"만 알아도 원인의 절반은 좁혀집니다.
태스크 단위로 쪼개는 이유는 두 가지예요. 첫째, 태스크 사이 의존성만 지켜지면 서로 다른 레시피의 태스크를 병렬로 돌릴 수 있고, 둘째 태스크 각각의 결과를 캐시해서 다음 빌드 때 건너뛸 수 있기 때문입니다. 후자가 4장에서 다룰 sstate 캐시예요.
1.2 작업은 전부 work 디렉터리에서 일어난다
모든 태스크는 레시피별 작업 디렉터리(WORKDIR) 안에서 실행돼요. 공식 문서의 빌드 디렉터리 구조 설명을 보면:
경로는 tmp/work/<아키텍처>/<레시피 이름>/<버전>/ 꼴이에요. 예를 들어 busybox 빌드가 이상하면 tmp/work/core2-64-poky-linux/busybox/ 아래로 들어가서 풀린 소스, 빌드 산출물, 태스크별 로그를 전부 눈으로 확인할 수 있습니다. 이 디렉터리 구조가 손에 익으면 Yocto 디버깅 속도가 완전히 달라져요 — 17회차(빌드 디버깅)에서 로그 파일들을 본격적으로 팝니다.
1.3 파이프라인 전체 지도
오늘 다룰 흐름을 그림 한 장으로 먼저 보고 시작할게요. 위쪽이 레시피 하나가 겪는 태스크 사슬이고, 각 단계 결과물이 어디로 가는지가 화살표입니다.
📌 실무 팁 — 특정 레시피의 태스크 목록이 궁금하면 bitbake busybox -c listtasks로 언제든 확인할 수 있어요. 공식 문서(Reference Manual, Manually Called Tasks)가 "Lists all defined tasks for a target."이라고 정의하는, 파싱만 하고 빌드는 하지 않는 안전한 명령입니다.
do_fetch는 2회차에서 본 레시피의 SRC_URI 변수를 읽고, URL의 접두어(https://, git://, file:// 등)에 맞는 fetcher 모듈을 골라 소스를 내려받아요. 받은 원본 압축파일은 build/downloads/(변수로는 DL_DIR)에 쌓이는데, 공식 문서가 "여러 빌드에서 재사용하거나 다른 위치로 옮겨도 된다"고 명시하는 디렉터리라 팀에서 공유 캐시로 자주 씁니다. do_unpack은 이걸 작업 디렉터리에 풀어놓는 단계예요.
ℹ️ 버전 참고 — 최신 개발판(6.0-tip) 문서는 소스가 풀리는 위치를 UNPACKDIR라는 별도 변수로 설명해요. LTS 기준으로는 "WORKDIR 아래에 풀린다"고 이해하면 충분하고, 소스의 최종 위치는 S 변수가 가리킨다는 점은 버전과 무관하게 같습니다.
2.2 do_patch — 패치 적용
소스를 풀었으면 우리 제품에 필요한 수정을 얹을 차례입니다.
"The do_patch task uses a recipe's SRC_URI statements and the FILESPATH variable to locate applicable patch files." — 같은 문서, Patching
SRC_URI에 나열된 *.patch·*.diff 파일들이 소스 디렉터리(S)에 순서대로 적용돼요. 2회차에서 본 .bbappend로 벤더 레시피에 패치를 추가하는 실무 패턴이 바로 이 단계에 끼어드는 것입니다. 패치가 소스 버전과 안 맞아 실패하면 do_patch 에러가 나요 — 벤더 BSP를 새 Yocto 버전으로 올릴 때 가장 먼저 깨지는 단골 지점입니다.
2.3 do_configure · do_compile · do_install — 설정, 컴파일, 설치
여기부터가 흔히 생각하는 "빌드"예요. 세 태스크의 공식 정의를 나란히 보면 역할 분담이 선명합니다:
"do_configure: Configures the source by enabling and disabling any build-time and configuration options for the software being built." "do_compile: Compiles the source code. This task runs with the current working directory set to ${B}." "do_install: Copies files that are to be packaged into the holding area ${D}." — Yocto Project Reference Manual, Tasks
변수 세 개만 기억하면 돼요. S는 소스가 있는 곳, B는 컴파일이 일어나는 곳(기본은 S와 동일), D는 make install의 결과가 들어가는 가짜 루트 디렉터리입니다. 타깃 보드의 /usr/bin에 설치될 파일은 실제로는 ${D}/usr/bin에 놓여요 — 빌드 호스트를 오염시키지 않으면서 "설치된 모습"을 만들어 두는 것이죠.
2회차에서 본 inherit autotools 같은 클래스 상속이 여기서 힘을 발휘해요. 클래스가 do_configure/do_compile/do_install의 표준 구현을 제공하니, 정형화된 빌드 방식을 쓰는 소프트웨어라면 레시피에서 이 함수들을 직접 쓸 일이 없습니다.
3. sysroot와 패키징 — 빌드 결과가 흘러가는 길
3.1 do_populate_sysroot — 다른 레시피를 위한 준비물
레시피 A가 라이브러리라면, 그걸 링크해야 하는 레시피 B는 A의 헤더와 .so를 어디서 찾을까요? 답이 sysroot입니다.
${D}에 설치된 파일 중 다른 레시피의 빌드에 필요한 것들(헤더, 라이브러리 등)만 골라 sysroot로 복사해 두는 단계예요. 중요한 건 Yocto가 레시피별 sysroot(recipe-specific sysroot)를 쓴다는 점 — 각 레시피는 빌드 직전에 do_prepare_recipe_sysroot 태스크가 자신이 DEPENDS로 선언한 것들만 모아 준 전용 sysroot(recipe-sysroot, 호스트 도구용은 recipe-sysroot-native)를 봐요. 선언 안 한 의존성은 아예 보이지 않으니, "내 PC에선 되는데 Yocto에선 헤더를 못 찾는" 상황은 대부분 DEPENDS 누락입니다.
3.2 do_package — 설치 결과를 패키지로 쪼개기
"The do_package and do_packagedata tasks combine to analyze the files found in the D directory and split them into subsets." — 같은 문서, Package Splitting
${D}의 내용물이 통째로 하나의 패키지가 되는 게 아니에요. 실행 파일은 busybox, 디버그 심볼은 busybox-dbg, 개발 헤더는 busybox-dev, 문서는 busybox-doc 식으로 용도별 서브패키지로 분할됩니다. 그 다음 do_package_write_rpm(또는 _ipk/_deb) 태스크가 실제 패키지 파일을 만들고, 최종 이미지는 do_rootfs 태스크가 이 패키지들을 골라 설치해 루트 파일시스템을 조립하는 것으로 완성돼요.
📌 실무 팁 — 타깃에 파일이 안 들어가 있을 때 추적 순서는 "${D}에 설치는 됐나(do_install) → 어느 서브패키지로 분류됐나(do_package) → 그 패키지가 이미지에 포함됐나(do_rootfs)"예요. 단계마다 산출물 디렉터리가 분리돼 있어서 어디까지 왔는지 눈으로 확인할 수 있습니다. 패키지 분할 규칙(FILES 변수)은 13회차에서 자세히 다뤄요.
4. sstate 캐시 — "다시 빌드하지 않는" 기술
4.1 왜 필요한가 — 처음부터 빌드의 딜레마
Yocto는 기본적으로 모든 것을 소스부터 빌드해요. 재현성은 완벽하지만, 공식 문서도 인정하는 대가가 있습니다:
"…building from scratch also takes much longer as it generally means rebuilding things that do not necessarily need to be rebuilt." — 같은 문서, Shared State Cache
레시피 하나 고쳤다고 수백 개 레시피를 전부 다시 빌드할 수는 없죠. 그래서 Yocto는 태스크 단위로 "이 태스크는 입력이 안 바뀌었으니 지난번 결과를 재사용해도 된다"를 판별하는 장치를 만들었고, 그게 공유 상태 캐시(shared state cache, 줄여서 sstate)입니다. 처음 빌드가 몇 시간, 두 번째가 몇 분인 이유가 전부 여기 있어요.
4.2 체크섬(시그니처)과 스탬프 — "바뀌었는가"의 판별
"The shared state code uses a checksum, which is a unique signature of a task's inputs, to determine if a task needs to be run again." — 같은 문서, Checksums (Signatures)
핵심 아이디어는 태스크의 모든 입력을 해시 하나로 요약하는 거예요. 태스크가 참조하는 변수 값, 실행할 스크립트 내용, 의존 태스크의 해시까지 전부 계산에 들어가요. 그래서 레시피에서 변수 하나만 바꿔도 관련 태스크의 해시가 달라지고, BitBake는 그 태스크(와 그에 의존하는 후속 태스크들)만 다시 실행합니다. 반대로 빌드 경로처럼 결과물에 영향 없는 값(WORKDIR 등)은 해시 계산에서 제외돼요.
성공한 태스크는 tmp/stamps/에 스탬프 파일을 남기는데, 기본 시그니처 핸들러(OEBasicHash)에서는 스탬프 파일 이름에 태스크 해시가 포함돼요. 메타데이터가 바뀌면 해시가 달라져 기존 스탬프와 일치하지 않게 되고, 자동으로 재실행이 걸리는 구조입니다.
4.3 setscene — 빌드를 건너뛰고 결과만 배치
해시가 일치하는 캐시를 찾았을 때 실제로 일어나는 일이 setscene 태스크예요.
"The idea of a setscene task (i.e do_taskname_setscene) is a version of the task where instead of building something, BitBake can skip to the end result and simply place a set of files into specific locations as needed." — 같은 문서, Setscene Tasks and Shared State
빌드 로그에 do_populate_sysroot_setscene 같은 이름이 잔뜩 지나가는 걸 본 적 있을 거예요 — 컴파일을 다시 한 게 아니라 캐시된 결과물을 제자리에 풀어놓기만 한 것입니다. 공식 문서 기준으로 setscene 변형이 있는 대표 태스크는 do_package, do_package_write_*, do_deploy, do_packagedata, do_populate_sysroot — 즉 "산출물을 내놓는" 태스크들이에요. do_fetch나 do_compile 같은 중간 과정 태스크는 최종 산출물이 캐시에서 나오면 아예 실행할 필요조차 없으니 setscene이 없습니다.
캐시 파일 자체는 build/sstate-cache/(변수 SSTATE_DIR)에 쌓이고, BitBake는 여기와 SSTATE_MIRRORS에 지정된 원격 위치를 함께 뒤져요:
"Behind the scenes, the shared state code works by looking in SSTATE_DIR and SSTATE_MIRRORS for shared state files." — 같은 문서, Setscene Tasks and Shared State
📌 실무 팁 — 팀 개발에서 sstate의 진가는 SSTATE_MIRRORS로 사내 서버의 캐시를 공유할 때 나와요. CI가 밤에 채워 둔 캐시를 팀원 전원이 받아 쓰면 신규 입사자의 첫 빌드도 극적으로 짧아집니다. 그리고 "캐시가 꼬인 것 같다" 싶을 때 tmp/를 통째로 지우는 건 괜찮지만(sstate-cache가 있으면 금방 복원돼요), sstate-cache까지 무턱대고 지우는 건 처음부터 재빌드를 의미하니 신중하게 — 특정 레시피만 bitbake -c cleansstate <recipe>으로 지우는 게 정석입니다(공식 정의: "Removes all output files and shared state (sstate) cache for a target.").
만약 bitbake 명령어 실행시 에러가 난다면, 에러 로그에서 설치하라는 설치 프로그램을 "sudo apt-get update" 후 설치 하고 진행하세요.
cd poky
# 1. 태스크 파이프라인의 정의부 찾기 — 기본 태스크들이 등록되는 클래스
grep -rn "addtask" meta/classes-global/base.bbclass | head -20
# 기대: do_fetch/do_unpack/do_patch 등이 addtask로 등록되고
# "after do_xxx" 구문으로 실행 순서 의존성이 걸려 있는 줄들이 보입니다.
# 2. sstate 로직의 실체 확인 — 클래스 파일로 존재
ls meta/classes-global/ | grep sstate
# 기대: sstate.bbclass — 4장에서 본 캐시 동작을 구현한 클래스입니다.
# 3. (빌드 환경을 이미 만들었다면) busybox의 전체 태스크 목록 보기
# 출처: Reference Manual "Manually Called Tasks" — do_listtasks:
# "Lists all defined tasks for a target."
source oe-init-build-env
bitbake busybox -c listtasks
# 기대: do_fetch, do_unpack, do_patch, do_configure, do_compile, do_install,
# do_package, do_populate_sysroot 그리고 *_setscene 변형들이 목록에 나타납니다.
✍️ 내 실행 결과 1. 태스크 파이프라인의 정의부 찾기 2. sstate 로직의 실체 확인
3. busybox의 전체 태스크 목록 보기 - source oi-init-build-env && bitbake busybox -c listtasks 결과
마무리할게요. 오늘은 bitbake 한 줄 뒤에서 돌아가는 태스크 파이프라인(do_fetch → do_unpack → do_patch → do_configure → do_compile → do_install → 패키징)과, 레시피 사이에 빌드 결과를 전달하는 sysroot, 그리고 "입력의 해시가 같으면 결과를 재사용한다"는 sstate 캐시의 원리를 봤어요. 개념부(A부)는 여기까지입니다. 다음 4회차부터는 드디어 손을 움직여요 — 개발 환경 준비하기: 호스트 PC 요구사항 확인부터 poky 클론까지, 첫 빌드를 위한 밑작업을 합니다.
댓글