Buildroot 패키지 시스템 분석 — .mk와 Config.in 동작 추적
#한 줄 요약
“한 패키지는
Config.in+<name>.mk두 파일이 전부입니다.” — Kconfig가 옵션을 노출하고,.mk가 빌드 명령을 정의합니다. 두 파일의 짝만 이해하면 2,000개 패키지 모두가 같은 패턴으로 풀립니다.
#왜 패키지 시스템부터 깊이 보는가
Buildroot로 실무에 들어가는 순간 가장 자주 만지는 곳이 패키지입니다. 새 라이브러리를 추가하거나, 회사 내 application을 rootfs에 넣거나, 기존 패키지의 빌드 옵션을 바꾸는 일이 매주 발생합니다. 다행히 Buildroot는 모든 패키지가 같은 패턴을 따르도록 강제합니다. 패턴 한 번 익히면 다음부터는 베끼고 수정이면 충분합니다.
이 장은 두 부분으로 나뉩니다. 전반은 Config.in + .mk 한 쌍의 구조, 후반은 generic-package / autotools-package / cmake-package 세 infra의 차이입니다.
#파일 두 개의 짝
package/<name>/ 안에 다음 두 파일이 한 쌍으로 들어갑니다.
package/myapp/├── Config.in ─ Kconfig 옵션 (menuconfig에 노출)└── myapp.mk ─ 빌드 레시피 (make가 실행)추가로 자주 있는 파일은 다음과 같습니다.
| 파일 | 역할 |
|---|---|
myapp.hash | tarball SHA-256, license 파일 hash |
0001-*.patch | source에 적용할 패치 |
S99myapp | /etc/init.d에 설치할 init script |
myapp.service | systemd unit |
이 디렉터리가 메인 트리(package/) 어디서 처음 등장하려면, 같은 디렉터리 안의 부모 Config.in에 한 줄을 추가해야 합니다.
# package/Config.in (해당 카테고리)source "package/myapp/Config.in"이 한 줄로 menuconfig 트리에 노출됩니다.
#Config.in — Kconfig 옵션 노출
Config.in은 옵션 정의입니다. 패키지를 켤지 끌지, 어떤 변형을 쓸지 같은 선택지를 사용자에게 보여 줍니다. 가장 단순한 예는 다음과 같습니다.
config BR2_PACKAGE_MYAPP bool "myapp" depends on BR2_USE_MMU depends on BR2_TOOLCHAIN_HAS_THREADS select BR2_PACKAGE_OPENSSL help My application that does a thing.
https://example.com/myapp각 줄의 의미는 다음과 같습니다.
| 구문 | 의미 |
|---|---|
config BR2_PACKAGE_MYAPP | 옵션 심볼. 반드시 BR2_PACKAGE_<UPPERNAME> 형태. |
bool "myapp" | bool 타입, menuconfig 표시 이름. |
depends on ... | 충족돼야 옵션이 보입니다. |
select ... | 함께 켜집니다 (의존성 강제). |
help ... | ? 키로 보이는 설명. |
자주 쓰는 depends on은 다음과 같습니다.
| 표현 | 의미 |
|---|---|
BR2_USE_MMU | MMU가 있는 target만 |
BR2_TOOLCHAIN_HAS_THREADS | pthread 가능 |
BR2_INSTALL_LIBSTDCPP | C++ runtime이 있는 toolchain |
BR2_TOOLCHAIN_HEADERS_AT_LEAST_5_0 | 커널 헤더 ≥ 5.0 |
!BR2_STATIC_LIBS | 정적 링크 금지 |
select는 조용히 함께 켜는 방식이라 의존성이 강제됩니다. 반면 depends on은 사용자가 직접 의존성을 켜야 합니다. 두 패턴 모두 표준이며, 패키지가 없으면 빌드가 깨지는 경우에는 select를 씁니다.
조건부 sub-옵션도 흔합니다.
config BR2_PACKAGE_MYAPP bool "myapp"
if BR2_PACKAGE_MYAPP
config BR2_PACKAGE_MYAPP_WITH_FOO bool "with foo support" default y
config BR2_PACKAGE_MYAPP_BACKEND string "backend name" default "default"
endifif … endif 블록은 부모 옵션이 켜진 경우에만 자식 옵션을 보여 줍니다. menuconfig 트리에서 자연스러운 들여쓰기가 일어납니다.
#.mk — 빌드 레시피
<name>.mk가 실제로 빌드를 수행합니다. 형식은 다음의 골격을 따릅니다.
################################################################################## myapp#################################################################################
MYAPP_VERSION = 1.2.3MYAPP_SOURCE = myapp-$(MYAPP_VERSION).tar.gzMYAPP_SITE = https://example.com/releasesMYAPP_LICENSE = MITMYAPP_LICENSE_FILES = LICENSEMYAPP_DEPENDENCIES = openssl
# (infra 선택 — 아래 절에서 설명)$(eval $(generic-package))규약은 철저히 대문자 이름 prefix입니다. 패키지 이름이 myapp이면 모든 변수가 MYAPP_*입니다. 이 prefix 규약을 깨는 패키지는 Buildroot CI가 거부합니다.
핵심 변수들은 다음 표가 거의 전부입니다.
| 변수 | 의미 | 예 |
|---|---|---|
<NAME>_VERSION | 버전 문자열 | 1.2.3 |
<NAME>_SITE | source URL prefix | https://... |
<NAME>_SOURCE | tarball 파일명 (보통 자동 유추) | myapp-1.2.3.tar.gz |
<NAME>_SITE_METHOD | http / git / local / svn | git |
<NAME>_LICENSE | SPDX-style 라이선스 | GPL-2.0+ |
<NAME>_LICENSE_FILES | source 내 라이선스 파일 | COPYING LICENSE |
<NAME>_DEPENDENCIES | 빌드 의존 패키지 | openssl libcurl |
<NAME>_INSTALL_STAGING | staging에 설치할지 | YES (라이브러리만) |
<NAME>_INSTALL_TARGET | rootfs에 설치할지 | YES (기본값) |
#세 가지 infra — generic / autotools / cmake
마지막 줄의 $(eval $(generic-package))가 infra를 선택합니다. 패키지의 빌드 시스템이 무엇인지에 따라 골라 씁니다.
| infra | 적용 빌드 시스템 | 호출 |
|---|---|---|
| generic-package | 표준 빌드 시스템이 없는 경우 (custom Makefile, 직접 스크립트 등) | $(eval $(generic-package)) |
| autotools-package | ./configure && make (GNU autotools) | $(eval $(autotools-package)) |
| cmake-package | cmake -B build && cmake --build build | $(eval $(cmake-package)) |
| meson-package | meson setup build && ninja -C build | $(eval $(meson-package)) |
| python-package | Python setup.py / pyproject | $(eval $(python-package)) |
| kconfig-package | 자체 Kconfig 가진 패키지 (busybox, linux) | $(eval $(kconfig-package)) |
대부분의 오픈소스 라이브러리는 autotools나 cmake입니다. 사내 application은 generic-package로 시작하는 경우가 많습니다. 각각 자세히 봅니다.
#generic-package — 가장 유연한 출발점
빌드 시스템이 비표준이라면 generic-package입니다. 세 hook을 명시적으로 정의합니다.
MYAPP_VERSION = 1.0MYAPP_SITE = $(call github,acme,myapp,v$(MYAPP_VERSION))MYAPP_LICENSE = Apache-2.0MYAPP_LICENSE_FILES = LICENSE
define MYAPP_BUILD_CMDS $(MAKE) $(TARGET_CONFIGURE_OPTS) -C $(@D) allendef
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.confendef
$(eval $(generic-package))이 코드의 움직이는 부분은 다음과 같습니다.
$(@D)—output/build/myapp-1.0/. 패키지 빌드 디렉터리.$(TARGET_CONFIGURE_OPTS)—CC=aarch64-...-gcc,CXX=...,CFLAGS=...등 cross-compile 변수가 한 묶음.$(TARGET_DIR)—output/target/. staged rootfs.$(STAGING_DIR)— staging (헤더·라이브러리 설치용).$(INSTALL)—install명령의 안전한 wrapper.
<NAME>_CONFIGURE_CMDS를 정의하지 않으면 configure 단계를 건너뜁니다. 단순 Makefile 패키지에 자주 발생하는 패턴입니다.
$(call github,acme,myapp,v1.0)은 https://github.com/acme/myapp/archive/v1.0.tar.gz로 펼쳐지는 helper입니다. 자주 쓰는 helper 몇 개를 알아두면 좋습니다.
| helper | 펼쳐지는 결과 |
|---|---|
$(call github,owner,repo,tag) | https://github.com/owner/repo/archive/<tag>.tar.gz |
$(call gitlab,owner,repo,tag) | https://gitlab.com/owner/repo/-/archive/<tag>/... |
$(call kernel,longterm,v6.x,linux-6.6.30) | kernel.org 미러 |
#autotools-package — ./configure && make
대부분의 GNU/오픈소스 라이브러리는 autotools입니다. infra가 configure·build·install을 모두 자동 처리합니다.
LIBFOO_VERSION = 2.5.0LIBFOO_SOURCE = libfoo-$(LIBFOO_VERSION).tar.xzLIBFOO_SITE = https://example.com/libfoo/releasesLIBFOO_LICENSE = LGPL-2.1+LIBFOO_LICENSE_FILES = COPYINGLIBFOO_INSTALL_STAGING = YESLIBFOO_DEPENDENCIES = host-pkgconf zlib
LIBFOO_CONF_OPTS = \ --disable-static \ --enable-shared \ --without-docs
ifeq ($(BR2_PACKAGE_LIBFOO_WITH_TLS),y)LIBFOO_CONF_OPTS += --enable-tlsLIBFOO_DEPENDENCIES += opensslelseLIBFOO_CONF_OPTS += --disable-tlsendif
$(eval $(autotools-package))자주 쓰는 변수들은 다음입니다.
| 변수 | 의미 |
|---|---|
<NAME>_CONF_OPTS | ./configure에 전달할 옵션 |
<NAME>_CONF_ENV | ./configure 호출 시 환경 변수 |
<NAME>_MAKE_OPTS | make에 전달할 옵션 |
<NAME>_MAKE_ENV | make 호출 시 환경 변수 |
<NAME>_INSTALL_STAGING_OPTS | staging install 옵션 |
<NAME>_INSTALL_TARGET_OPTS | target install 옵션 |
LIBFOO_INSTALL_STAGING = YES가 라이브러리 패키지의 표지입니다. staging에 헤더·.so·.pc를 설치해, 다른 패키지가 빌드 시 link할 수 있게 합니다. 실제 rootfs에는 INSTALL_TARGET이 별도로 처리합니다.
#cmake-package — CMake 빌드
cmake 기반 라이브러리도 그대로 표준 패턴이 있습니다.
LIBBAR_VERSION = 1.0.0LIBBAR_SOURCE = libbar-$(LIBBAR_VERSION).tar.gzLIBBAR_SITE = https://example.com/libbarLIBBAR_LICENSE = MITLIBBAR_LICENSE_FILES = LICENSE.txtLIBBAR_INSTALL_STAGING = YESLIBBAR_DEPENDENCIES = host-cmake
LIBBAR_CONF_OPTS = \ -DBUILD_SHARED_LIBS=ON \ -DBUILD_TESTING=OFF \ -DENABLE_DOCS=OFF
$(eval $(cmake-package))cmake-package는 toolchain file을 자동으로 적용합니다. cross-compile에 필요한 CMAKE_C_COMPILER, CMAKE_FIND_ROOT_PATH, CMAKE_SYSROOT 등이 한 번에 세팅됩니다. 패키지 작성자는 옵션만 관리하면 됩니다.
#python-package — Python 모듈
Python application·라이브러리는 별도의 infra가 있습니다.
PYTHON_MYTOOL_VERSION = 0.9PYTHON_MYTOOL_SOURCE = mytool-$(PYTHON_MYTOOL_VERSION).tar.gzPYTHON_MYTOOL_SITE = https://example.com/mytoolPYTHON_MYTOOL_LICENSE = Apache-2.0PYTHON_MYTOOL_LICENSE_FILES = LICENSEPYTHON_MYTOOL_SETUP_TYPE = pyprojectPYTHON_MYTOOL_DEPENDENCIES = host-python-flit-core
$(eval $(python-package))<NAME>_SETUP_TYPE이 setuptools / flit / pyproject / distutils 중 하나입니다. 각각 어떤 도구로 빌드할지를 가립니다.
#host- prefix — 호스트용 패키지
같은 패키지를 호스트에서도 쓰고 싶을 때가 있습니다. 예를 들어 pkg-config, cmake, python-flit-core는 빌드 도중 호스트가 사용합니다. 이때는 host-<name> 형태로 별도 빌드합니다.
Config.in은 같지만 dependencies에 host-pkgconf처럼 prefix를 붙입니다. .mk에는 추가로 다음 한 줄을 더합니다.
$(eval $(host-autotools-package)) # 또는 host-cmake-package 등이 한 줄로 같은 패키지가 호스트용으로도 빌드되어 output/host/에 설치됩니다. 흔히 target + host 둘 다 필요한 경우 두 줄을 함께 둡니다.
#.hash — 무결성 검증
source tarball을 받은 뒤 hash 검증을 합니다. package/<name>/<name>.hash가 정답을 담고 있습니다.
# Locally calculatedsha256 ab12cd34ef... myapp-1.2.3.tar.gzsha256 56ef78ab90... LICENSEmake myapp-source를 처음 돌릴 때 hash가 생성되지 않으면 CI가 거부합니다. 새 버전을 올릴 때 다음 명령으로 hash를 다시 계산합니다.
make myapp-dircleanrm dl/myapp/*make myapp-sourcesha256sum dl/myapp/myapp-1.3.0.tar.gzsha256sum 결과를 myapp.hash에 갱신합니다.
license 파일도 hash로 검증합니다. 이는 업스트림이 라이센스를 조용히 바꾸는 것을 catch하기 위한 안전장치입니다.
#패치 — package 디렉터리 안의 0001-*.patch
source에 수정이 필요하면 직접 source를 고치지 말고 패치 파일을 둡니다.
package/myapp/├── Config.in├── myapp.hash├── myapp.mk├── 0001-fix-cross-compile.patch└── 0002-add-musl-support.patch번호 순서대로(0001, 0002, …) 적용됩니다. git format-patch 출력 그대로 둘 수 있어 상류 PR 보내기도 자연스럽습니다.
#디버깅 흐름
패키지를 작성하다 보면 빌드가 깨졌는데 어디인지 모를 때가 옵니다. 다음 흐름이 표준입니다.
make V=1 myapp 2>&1 | tee build.logless build.logcd output/build/myapp-1.2.3/make CC=$(realpath ../../host/bin/aarch64-...-gcc) ...V=1로 verbose 빌드 후 build.log에서 실제 명령을 찾고, 압축 풀린 source 디렉터리로 들어가 손으로 명령을 재현해 문제 위치를 좁힙니다.
특히 효과적인 명령은 다음입니다.
make myapp-dircleanmake myapp-extractmake myapp-patchmake myapp-configuremake myapp-buildmake myapp-install순서대로 source 디렉터리 삭제, 새로 압축 풀기, 패치 적용까지, configure, build, install입니다.
단계별로 끊어 들어가면 어느 단계에서 실패하는지 명확합니다. 각 단계의 산물은 output/build/myapp-<ver>/.stamp_<단계>로 표시되므로, 해당 stamp 파일을 지우면 그 단계부터 재실행합니다.
여섯 단계의 진행과 각 단계에 대응되는 make 타깃, stamp 파일을 한 장으로 정리하면 다음과 같습니다.
#자주 하는 실수
<NAME>_*변수 prefix를 패키지명과 안 맞춥니다. 패키지가 조용히 무시됩니다. 항상make show-info | grep <name>으로 인식 여부 확인.<NAME>_LICENSE를 비웁니다. maintainer CI가 reject. 정말 모르겠으면LICENSE = unknown으로라도 명시.<NAME>_DEPENDENCIES에 host 도구를 빠뜨립니다. 예를 들어host-pkgconf. 다른 사람 머신에서 빌드 깨집니다.- source 안에서 직접 수정합니다. 다음
make clean한 번이면 사라집니다. 변경은 반드시0001-*.patch로 영구화. - staging vs target 혼동. 라이브러리는 staging에도 가야 다른 패키지가 빌드 시 link할 수 있습니다.
INSTALL_STAGING = YES를 빠뜨리지 마세요.
#정리
- 한 패키지는
Config.in(옵션 노출) +<name>.mk(빌드 레시피) 두 파일로 정의됩니다. Config.in의 옵션 심볼은 반드시BR2_PACKAGE_<UPPER>형태입니다..mk는<NAME>_VERSION·SITE·LICENSE·DEPENDENCIES같은 변수 + infra 호출 한 줄로 동작합니다.- infra는
generic-package/autotools-package/cmake-package/meson-package/python-package가 가장 흔합니다. - 라이브러리는
INSTALL_STAGING = YES로 staging에 헤더·.so·.pc를 함께 설치합니다. <name>.hash가 source와 license 파일의 무결성을 검증합니다.- 변경은 source 직접 수정 금지,
0001-*.patch로 영구화합니다. - 빌드 디버깅은
make <pkg>-extract/patch/configure/build/install을 단계별로 끊어 갑니다.
#다음 장 예고
다음 편은 Ch 6: 외부 트리 — BR2_EXTERNAL. 회사·팀의 패키지·보드를 Buildroot 본체와 분리하는 메커니즘을 다룹니다.
#관련 항목
Buildroot Practical · 5 of 20
- 1Buildroot가 푸는 문제 — Yocto와의 핵심 차이 분석
- 2Buildroot 디렉터리 구조 분해 — board·configs·dl·output
- 3Buildroot Kconfig 설정 — menuconfig와 defconfig 작성
- 4Buildroot 첫 빌드 — QEMU에서 동작하는 시스템 만들기
- 5Buildroot 패키지 시스템 분석 — .mk와 Config.in 동작 추적
- 6Buildroot 외부 트리 — BR2_EXTERNAL 구성과 활용
- 7Buildroot 보드 Customize — overlay·post-build·post-image 흐름
- 8Buildroot 출력 파일시스템 — initramfs·squashfs·ext4·cpio 선택
- 9Buildroot 새 패키지 작성 — autotools·cmake·python 통합
- 10Buildroot 실전 — BeagleBone Black 시스템 처음부터 끝까지
- 11Buildroot Toolchain 선택 — Internal vs External 비교
- 12Buildroot 커널 Customize — defconfig fragment와 DTS 통합
- 13Buildroot U-Boot 통합 — 빌드·env·fw_env 흐름
- 14Buildroot 빌드 캐싱 분석 — dl·ccache·per-package
- 15Buildroot post-build·post-image 심화 — rootfs 최종 수정 흐름
- 16Buildroot OTA 이미지 업데이트 — RAUC·swupdate 통합
- 17Buildroot SDK 생성·배포 — make sdk와 application 워크플로
- 18Buildroot Security·CVE 추적 — pkg-stats와 Reproducible Builds
- 19Buildroot CI/CD 구축 — Container Build와 Cache 공유
- 20Buildroot → Yocto 마이그레이션 — 언제·어떻게 옮길까
관련 글
Buildroot 새 패키지 작성 — autotools·cmake·python 통합
Buildroot에 새 패키지를 추가하는 실전 — autotools·cmake·python 세 케이스.
Buildroot → Yocto 마이그레이션 — 언제·어떻게 옮길까
Buildroot가 한계에 도달하는 신호와 Yocto/OE로 점진 이전하는 패턴, meta-buildroot 같은 hybrid 옵션.
Buildroot CI/CD 구축 — Container Build와 Cache 공유
GitLab/GitHub Actions에서 Buildroot 트리를 컨테이너로 빌드하고 dl·ccache를 팀이 공유하는 패턴.