본문으로 건너뛰기
Buildroot Practical · 20/20

Buildroot → Yocto 마이그레이션 — 언제·어떻게 옮길까

· Hawk · 12분 읽기

#한 줄 요약

“Yocto로 옮길지의 결정은 기능이 아니라 조직과 제품 라인이 답을 줍니다.” — board 수, 팀 크기, vendor BSP 방향이 한계의 신호이며 migration은 한 번에가 아니라 점진적으로 진행하는 것이 합리적입니다.

이 시리즈의 첫 장에서 두 시스템의 철학적 트레이드오프를 다뤘습니다. 마지막 장에서는 그 트레이드오프가 실무에서 어떤 신호로 드러나는지, 그리고 옮길 결심이 섰을 때 어떤 순서로 진행하는지를 정리합니다. Buildroot에서 시작한 프로젝트가 성장하면 어느 시점에 Yocto 쪽이 자연스러워지는 변곡점이 옵니다. 그 변곡점을 식별하는 안목이 이 장의 목표입니다.

#언제 Buildroot의 한계가 보이는가

다음 다섯 가지 신호가 동시에 또는 순차적으로 나타나면 이전을 진지하게 검토할 때입니다.

신호 1 — 보드 수가 3개 이상으로 늘어남

같은 SoC family의 변형 보드를 동시에 유지하는 순간 Buildroot의 한 트리 한 타깃 원칙이 비용으로 돌아옵니다. 보드별로 별도의 트리를 두면 패치가 세 번씩 적용돼야 하고, 단일 트리에 BR2_EXTERNAL로 묶으면 defconfig가 폭주합니다.

신호 2 — per-package 분기가 폭주함

특정 보드에서만 패키지의 다른 버전이 필요하거나, 특정 조건에서만 다른 옵션을 쓰는 분기가 늘어나면 Config.inif-then-else 트리로 변합니다. recipe별 격리가 없는 구조의 약점이 드러나는 지점입니다.

신호 3 — vendor BSP가 Yocto-only로 발표됨

NXP·TI·Xilinx·Qualcomm 같은 SoC vendor는 새 chip을 발표할 때 meta-imx·meta-ti·meta-xilinx 같은 Yocto 레이어로만 BSP를 배포하는 경우가 많습니다. Buildroot 트리에 vendor 패치를 직접 옮겨오는 작업이 매 분기 발생하면 그 비용이 마이그레이션 비용을 곧 넘어섭니다.

신호 4 — SDK를 외부 개발자에게 배포해야 함

application 개발팀과 platform 팀이 분리되고, application 팀이 별도의 SDK로 cross-compile해야 한다면 Yocto의 populate_sdk_ext가 결정적으로 편합니다. Buildroot의 make sdk로도 가능하지만 recipe 확장 가능한 eSDK 같은 기능이 없습니다.

신호 5 — CI 시간이 sstate를 매력적으로 보이게 함

PR마다 30분씩 빌드를 반복하면 한 달이면 팀 전체에서 수십 시간이 누적됩니다. Yocto의 sstate-cache는 signature 동일이면 거의 무료로 산출물을 재사용하므로 4번째 빌드부터는 Buildroot보다 빨라지는 경우가 흔합니다.

이 다섯 신호 중 셋 이상이 동시에 켜져 있다면 이전 검토를 시작해도 늦지 않습니다.

#migration 비용 추정

이전 비용은 코드 변환보다 팀 학습재테스트가 차지합니다. 시스템 규모별 대략의 견적입니다.

규모패키지 수보드 수recipe 변환팀 학습재테스트총합
소형50 이하1 ~ 21 ~ 2일2 ~ 3일2 ~ 3일약 1주
중형100 ~ 2502 ~ 41 ~ 2주1주1 ~ 2주약 1개월
대형500+5+4 ~ 6주2 ~ 3주3 ~ 4주약 3개월

소형 시스템은 한 사람 한 주면 끝나기도 합니다. 중형 이상은 한 명이 전담하기보다 팀 전체가 일부 시간을 떼어 진행하는 편이 안전합니다. 가장 큰 비용은 예상 못 한 ABI 차이로 인한 런타임 사고이며, 이는 재테스트 단계에 묶어 두는 게 통상입니다.

대형 시스템에서 가장 큰 함정은 recipe 100개를 한꺼번에 변환하려는 시도입니다. 점진적으로가 거의 항상 옳습니다.

#개념 매핑 — Buildroot ↔ Yocto

두 시스템은 같은 문제를 다른 어휘로 풉니다. 다음 표가 핵심 개념의 1

대응입니다.

BuildrootYocto/OE비고
package/<name>/<name>.mkrecipes-<group>/<name>/<name>_<ver>.bb패키지 정의
package/<name>/Config.inrecipe 안의 PACKAGECONFIG빌드 옵션
package/<name>/<name>.hashrecipe 안의 SRC_URI[sha256sum]무결성
package/<name>/*.patchrecipes-.../<name>/<name>/<patch>패치
configs/<board>_defconfigmeta-myproject/conf/machine/<board>.conf보드 정의
BR2_EXTERNALmeta-<layer> (bblayers.conf)외부 트리
BR2_PACKAGE_<X>=yIMAGE_INSTALL += "x"이미지에 패키지 포함
BR2_TARGET_GENERIC_HOSTNAMEhostname_pn-base-files = "..."시스템 변수
system/skeleton/recipes-core/base-files/ 안의 파일기본 rootfs 골격
make sdkbitbake -c populate_sdkSDK 빌드
output/images/rootfs.ext4tmp/deploy/images/<machine>/<image>.ext4산출물
make <pkg>-rebuildbitbake -c compile -f <recipe>강제 재빌드
dl/ cachedownloads/ + sstate-cache/다운로드와 sstate
BR2_TOOLCHAIN_EXTERNALTCMODE = "external-..."외부 toolchain

대응이 명확하지 않은 항목이 두 가지 있습니다. 첫째, Buildroot에는 per-recipe sysroot가 없으므로 Yocto의 recipe 격리를 그대로 옮길 수 없습니다. 둘째, Buildroot의 Config.in글로벌 dependency 그래프를 만들지만 Yocto는 recipe별 DEPENDS·RDEPENDS국소화합니다. 이전 시 글로벌 분기를 recipe별 분기로 재구성하는 작업이 필요합니다.

#점진적 이전 — hybrid 패턴

가장 안전한 이전은 한 번에 전환하지 않는 것입니다. 다음 두 가지 hybrid 패턴이 실무에서 자주 쓰입니다.

패턴 1 — 새 시스템은 Yocto, 기존은 Buildroot

기존 제품의 firmware는 그대로 Buildroot로 유지하고, 새로 시작하는 제품 라인만 Yocto로 진행합니다. 팀이 두 시스템을 병행하는 기간이 6 ~ 12개월 정도 생깁니다. 이 패턴의 장점은 기존 출시 일정에 영향 없음입니다.

패턴 2 — meta-buildroot으로 Buildroot 트리를 Yocto에 임베드

이 방식이 덜 알려졌지만 매우 유용합니다. meta-buildroot layer를 Yocto에 추가하면 Buildroot 트리 전체를 한 recipe로 감싸 빌드할 수 있습니다.

yocto-build/
├── sources/
│ ├── poky/ # Yocto 본가
│ ├── meta-openembedded/
│ ├── meta-imx/ # vendor BSP
│ └── meta-buildroot/ # bridge layer
├── buildroot/ # 기존 Buildroot 트리 그대로
│ ├── package/
│ ├── configs/
│ └── ...
└── build/
└── conf/
├── bblayers.conf
└── local.conf

meta-buildroot의 layer.conf 예시.

meta-buildroot/conf/layer.conf
BBPATH .= ":${LAYERDIR}"
BBFILES += "${LAYERDIR}/recipes-*/*/*.bb"
BBFILE_COLLECTIONS += "buildroot"
BBFILE_PATTERN_buildroot = "^${LAYERDIR}/"
BBFILE_PRIORITY_buildroot = "10"
LAYERSERIES_COMPAT_buildroot = "scarthgap kirkstone"

Buildroot 빌드 wrapper recipe.

meta-buildroot/recipes-core/buildroot-rootfs/buildroot-rootfs.bb
SUMMARY = "Buildroot-built rootfs wrapped as Yocto artifact"
LICENSE = "CLOSED"
S = "${TOPDIR}/../buildroot"
do_compile() {
cd ${S}
make ${BUILDROOT_DEFCONFIG}
make -j${BB_NUMBER_THREADS}
}
do_install() {
install -d ${D}/buildroot-output
cp -a ${S}/output/images/* ${D}/buildroot-output/
}
FILES:${PN} += "/buildroot-output"
BUILDROOT_DEFCONFIG ?= "qemu_aarch64_virt_defconfig"

이 구조가 주는 이득이 큽니다. 기존 Buildroot 트리는 그대로 유지하면서 Yocto의 sstate·layer·SDK 인프라를 점차 도입할 수 있습니다. 이전 기간 동안 패키지 하나씩 Buildroot에서 떼어내 OE recipe로 옮기는 작업을 점진적으로 진행하면 됩니다.

단점은 두 시스템의 비용을 모두 부담한다는 점입니다. 영구적인 구조로 권장하지 않으며 이전 기간 동안의 다리로 사용합니다.

#recipe 변환 예 — .mk → .bb

가장 흔한 변환은 작은 application 패키지입니다. Buildroot 쪽 정의.

################################################################################
#
# myapp
#
################################################################################
MYAPP_VERSION = 1.0
MYAPP_SITE = https://github.com/example/myapp/archive/refs/tags
MYAPP_SOURCE = myapp-$(MYAPP_VERSION).tar.gz
MYAPP_LICENSE = MIT
MYAPP_LICENSE_FILES = LICENSE
MYAPP_DEPENDENCIES = openssl libcurl
define MYAPP_BUILD_CMDS
$(MAKE) -C $(@D) CC="$(TARGET_CC)" CFLAGS="$(TARGET_CFLAGS)"
endef
define MYAPP_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/myapp $(TARGET_DIR)/usr/bin/myapp
$(INSTALL) -D -m 0644 $(@D)/myapp.conf $(TARGET_DIR)/etc/myapp.conf
endef
$(eval $(generic-package))

같은 패키지의 OE recipe.

SUMMARY = "MyApp — example application"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=abc1234567890abcdef1234567890abcd"
SRC_URI = "https://github.com/example/myapp/archive/refs/tags/myapp-${PV}.tar.gz"
SRC_URI[sha256sum] = "1111aaaa2222bbbb3333cccc4444dddd5555eeee6666ffff7777000088889999"
DEPENDS = "openssl curl"
S = "${WORKDIR}/myapp-${PV}"
do_compile() {
oe_runmake CC="${CC}" CFLAGS="${CFLAGS}"
}
do_install() {
install -D -m 0755 ${S}/myapp ${D}${bindir}/myapp
install -D -m 0644 ${S}/myapp.conf ${D}${sysconfdir}/myapp.conf
}
FILES:${PN} += "${sysconfdir}/myapp.conf"

차이는 다음과 같습니다. Buildroot는 make 함수(MYAPP_BUILD_CMDS)로 호출되는 표준 매크로 패턴인 반면, OE는 do_compile·do_install 같은 task 함수로 분해됩니다. 변수 이름도 다릅니다. Buildroot의 TARGET_CC가 OE에서는 그냥 CC이며 cross-compile이 암시적으로 활성화됩니다. TARGET_DIR${D}, /usr/bin${bindir}경로 변수화가 강제됩니다.

변환 시 가장 자주 놓치는 부분이 FILES:${PN} 선언입니다. OE는 패키지 분할을 자동으로 하므로 /etc 아래 설치한 파일이 main package에 포함되도록 명시해야 합니다. 이 한 줄을 빠뜨리면 이미지에 myapp.conf빠집니다.

#defconfig → MACHINE conf

Buildroot의 보드 정의는 한 줄짜리 configs/<board>_defconfig 파일에 모든 옵션이 들어 있습니다. OE의 동등물은 meta-<layer>/conf/machine/<machine>.conf 한 파일에 MACHINE 특성만 정의됩니다.

meta-myproject/conf/machine/mxc8mp.conf
#@TYPE: Machine
#@NAME: NXP i.MX 8M Plus EVK
#@DESCRIPTION: Custom board based on i.MX 8M Plus
require conf/machine/include/imx-base.inc
DEFAULTTUNE ?= "cortexa53-crypto"
require conf/machine/include/tune-cortexa53.inc
MACHINE_FEATURES += "pci wifi bluetooth"
KERNEL_DEVICETREE = "freescale/imx8mp-mxc8mp.dtb"
UBOOT_CONFIG ??= "sd"
UBOOT_CONFIG[sd] = "imx8mp_mxc8mp_defconfig,sdcard"
SERIAL_CONSOLES = "115200;ttymxc1"
IMAGE_FSTYPES += "wic.bz2 ext4"
WKS_FILE = "imx8mp-mxc8mp.wks.in"

이 한 파일이 Buildroot의 mxc8mp_defconfig에 해당합니다. 다만 그 안에 패키지 선택은 들어가 있지 않습니다. 패키지는 IMAGE_INSTALL이라는 별도 변수에서 image recipe가 관리합니다. 이 분리가 Yocto가 다중 보드를 깨끗하게 다루는 핵심입니다. 같은 image recipe로 mxc8mp, mxc8mp-lite, mxc8mp-debug 같은 변형을 모두 빌드할 수 있습니다.

meta-myproject/recipes-core/images/myproject-image.bb
SUMMARY = "MyProject base image"
LICENSE = "MIT"
inherit core-image
IMAGE_INSTALL += " \
myapp \
openssh \
curl \
python3 \
${MACHINE_EXTRA_INSTALL} \
"
IMAGE_FEATURES += "ssh-server-openssh"

여기에 보드별로만 다른 패키지가 있다면 MACHINE_EXTRA_INSTALL을 보드 conf 안에서 다르게 정의하면 됩니다. 분기recipe 본문이 아닌 MACHINE 변수로 격리됩니다.

#빌드 시간 비교

같은 “AArch64 + busybox + nginx + sshd + Python 3” 시스템을 양쪽으로 빌드한 측정값입니다. 8-core Ryzen 7 워크스테이션, NVMe SSD, 32 GB RAM, 캐시 없는 cold start 기준.

시나리오Buildroot 2024.02Yocto Scarthgap비고
First build (cold)32분110분sstate 없음
Second build (no change)5초8초변경 감지만
Second build (1 pkg 수정)25초12초sstate hit
Branch switch (200 pkgs)32분 (대부분 rebuild)14분 (sstate hit 90%)동일 baseline 트리
CI 4번째 빌드32분7분sstate 캐시 누적
Toolchain만 교체32분 (전체 재빌드)18분 (sstate signature 변경)gcc 13 → 14
Multi-board 5개 빌드5 × 32분 = 160분1 × 110분 + 4 × 8분 = 142분첫 보드 후 sstate 공유

Buildroot가 빠른 시나리오는 first buildsingle board의 incremental 두 가지뿐입니다. 팀 인원보드 수가 늘어나는 순간 모든 후속 시나리오에서 Yocto가 우세합니다.

Ch 14에서 다룬 Buildroot의 ccache + per-package directories로 일부 격차를 좁힐 수 있지만, cross-PR sstate 공유는 구조적으로 불가능합니다. 이 한 가지가 5번째 빌드부터의 차이를 만들어 냅니다.

#운영 비용 비교

빌드 시간 외의 조직 비용도 같이 봐야 합니다.

항목BuildrootYocto/OE
신규 팀원 학습1주1 ~ 2개월
빌드 시스템 전담불필요1명 (5인 이상 팀일 때)
CI 인프라runner 1대로 충분sstate 서버 + runner 여러 대
vendor BSP 적용매 분기 patches 정리새 layer 추가
장기 LTS 유지2년이 한계5 ~ 10년
다중 보드 추가 비용보드 1개당 주 단위보드 1개당 일 단위
SDK 외부 배포tarball, 확장 어려움eSDK, devtool 워크플로
빌드 디버깅make V=1로 즉시bitbake task graph, sstate 검증 필요

작은 팀과 단일 제품에서는 Buildroot의 학습 비용 1주가 무엇과도 바꿀 수 없는 자산입니다. 큰 팀과 다중 제품에서는 Yocto의 보드 추가 비용 일 단위가 결정적 자산입니다.

#옮기지 말아야 할 신호

이전 결정만큼 이전하지 않는 결정도 중요합니다. 다음 신호가 보이면 Yocto 도입은 과한 결정일 가능성이 높습니다.

  • 팀이 5명 이하이며 빌드 시스템 전담 인력이 없습니다.
  • board가 1개이고 향후 2년 내 추가 계획이 없습니다.
  • rootfs가 64 MB 이하이며 패키지 수가 50 이하입니다.
  • vendor BSP가 mainline에 잘 들어가 있어 vendor layer 의존이 약합니다.
  • 출시 일정이 6개월 이하로 임박해 지금 이전은 위험합니다.
  • OTA·SDK 배포가 불필요하고 flash 후 종료형 firmware입니다.

이 조건이 두세 개 겹치면 Yocto의 유연함은 그대로 비용입니다. Buildroot에 머무르면서 Ch 14 (caching), Ch 17 (SDK)에서 다룬 기술로 한계를 끌어올리는 편이 더 합리적입니다. 도구는 문제에 맞는 것이 답이며 더 큰 도구가 답인 경우는 의외로 드뭅니다.

#시리즈 마무리

20장에 걸쳐 Buildroot의 철학·구조·실전·운영·한계를 차례로 다뤘습니다. 회고로 정리합니다.

다룬 것
I. 기초Ch 1 ~ 5철학, 디렉터리, Kconfig, 첫 빌드, 패키지 그래프
II. 구성 요소Ch 6 ~ 10rootfs overlay, post-build·post-image, init system, networking, 실 보드
III. 커스터마이제이션Ch 11 ~ 15toolchain, kernel, device tree, BR2_EXTERNAL, caching
IV. 운영Ch 16 ~ 19release branching, SDK, OTA·secure boot, CI 통합
V. 미래Ch 20Yocto 이전 결정

이 시리즈를 졸업한 독자가 다음에 자연스럽게 향할 곳이 있습니다.

  • Yocto/OpenEmbedded 학습 — 가장 직접적인 후속. 시작점은 Yocto Mega-ManualOpenEmbedded Reference Manual. 한국어 후속 시리즈는 현재 준비 중이며, 이 블로그의 embedded/yocto 카테고리가 생기면 그쪽으로 이어집니다.
  • Linux from Scratch (LFS) — 빌드 시스템에 의존하지 않고 같은 시스템을 손으로 조립해 보는 경험. Buildroot가 무엇을 자동화하는지 가장 깊게 이해할 수 있습니다.
  • BSP Development 시리즈 — 빌드 시스템 아래의 BSP 계층. U-Boot, device tree, kernel BSP를 다룹니다.
  • Embedded C++ for Real Systems — userland 쪽으로 시선을 옮길 때.

마지막으로 한 가지 당부가 있습니다. 빌드 시스템은 수단입니다. 제품의 가치는 그 위에 올라가는 application과 사용자 경험에서 옵니다. 빌드 시스템에 너무 깊이 빠지지 않고 충분히 안다는 선에서 멈출 줄 아는 것도 엔지니어링의 일부입니다. 이 시리즈가 그 선을 찾는 데 도움이 됐기를 바랍니다.

#정리

  • Yocto 이전의 다섯 가지 신호는 board 수 3+, per-package 분기 폭주, vendor BSP가 Yocto-only, SDK 외부 배포, CI에 sstate가 매력적입니다.
  • 이전 비용은 소형 1주, 중형 1개월, 대형 3개월 정도이며 recipe 변환보다 팀 학습과 재테스트가 비용 대부분을 차지합니다.
  • 개념 매핑은 대체로 1
    per-recipe sysroot 부재글로벌 Config.in vs 국소 DEPENDS가 변환의 어려운 지점입니다.
  • 점진적 이전은 meta-buildroot로 Buildroot 트리를 Yocto에 임베드해 두 시스템을 다리 기간 동안 병행하는 패턴이 안전합니다.
  • recipe 변환의 핵심은 make 매크로 → task 함수, TARGET_CC → CC, *TARGET_DIR → D,그리고FILES:{D}*, 그리고 `FILES:{PN}` 명시입니다.
  • defconfig는 MACHINE conf로 MACHINE 특성만 옮기고 패키지 선택은 image recipe로 분리됩니다.
  • 4번째 빌드부터 Yocto가 빠르며, 5개 보드 빌드에서 sstate 공유로 누적 시간이 역전됩니다.
  • 옮기지 말아야 할 신호도 분명합니다. 팀 5명 이하·단일 보드·소형 rootfs라면 Yocto는 과한 결정입니다.
  • 도구는 문제에 맞는 것이 답이며, 더 큰 도구가 답인 경우는 의외로 드뭅니다.

#관련 항목