Yocto를 이용해 나만의 Rasberrypi Image를 만들어보기

Docker 이미지를 이용해 보겠다. Docker 이미지를 사용하면 호스트 시스템에 Yocto Project의 의존성을 설치하지 않고도 Yocto 빌드 환경을 쉽게 설정할 수 있다. Raspberrypi 재단에서 관리하는 https://github.com/agherzan/meta-raspberrypi에는 해당 저장소를 관리하기 위한 github action 설정이 존재하는데, 이들 github action들은 Docker환경에서 수행된다. 그리고 이 Docker환경을 구성하고 관리하기 위한 Dockerfile이 존재한다. 아쉽게도 One-step은 아니지만 해당 항목을 분석하면 좀 더 용이하게 rasberrypi image를 만들기 위한 Yocto 빌드 환경을 만들 수 있을 것이다.

yocto-dockerfiles 저장소를 클론하고

$ git clone https://github.com/agherzan/meta-raspberrypi.git
$ cd meta-raspberrypi/.github/workflows/docker-images

meta-raspberrypi/.github/workflows/docker-images/yocto-builder/Dockerfile을 살펴보면 Ubuntu 20.04를 베이스로 Yocto 빌드에 필수적인 패키지들을 설치하는 것을 확인할 수 있다. 하지만 이 Dockerfile 자체에는 Poky를 다운로드하는 내용이 없다. 좀 더 살펴볼 필요가 있다.

FROM ubuntu:20.04
...
RUN eatmydata apt-get install -qq -y \
    gawk wget git diffstat unzip texinfo gcc build-essential chrpath \
    socat cpio python3 python3-pip python3-pexpect xz-utils debianutils \
    iputils-ping python3-git python3-jinja2 libegl1-mesa libsdl1.2-dev \
    pylint3 xterm python3-subunit mesa-common-dev zstd liblz4-tool

Dockerfile의 마지막 부분을 보면 .github/workflows/docker-images/yocto-builder/entrypoint-build.sh 스크립트를 복사해 두는데, 실제 Poky 다운로드는 이 스크립트 실행 시점에 이루어진다. entrypoint-build.sh는 스크립트 전반에 걸쳐 많은 매개변수를 필요로 한다.

REPOS=" \
    git://git.yoctoproject.org/poky.git \
"
for repo in $REPOS; do
    log "Cloning $repo on branch $BASE_REF..."
    git clone --depth 1 --branch "$BASE_REF" "$repo"
done

# shellcheck disable=SC1091,SC2240
. ./poky/oe-init-build-env build

GitHub Action 설정 파일인 .github/workflows/yocto-builds.yml을 확인해 보면, 워크플로우 수행 시 Docker 컨테이너에 다양한 환경 변수를 주입하며 entrypoint-build.sh를 실행하는 것을 알 수 있다.

        docker run --rm \
            -v "$GITHUB_WORKSPACE:/work:ro" \
            -v "$DL_DIR:$DL_DIR:rw" \
            -v "$SSTATE_DIR:$SSTATE_DIR:rw" \
            --env "BASE_REF=$GITHUB_BASE_REF" \
            --env "MACHINE=${{ matrix.machine }}" \
            --env "DISTRO=${{ matrix.distro }}" \
            --env "IMAGE=${{ matrix.image }}" \
            --env "DL_DIR=$DL_DIR" \
            --env "SSTATE_DIR=$SSTATE_DIR" \
            "yocto-builder-${{ github.event.number }}" \
            /entrypoint-build.sh

다시 entrypoint-build.sh를 자세히 보면 레이어 추가부터 bitbake 실행까지 일괄 처리하도록 되어 있다.

# Add the BSP layer
bitbake-layers add-layer "$META_RASPBERRYPI_PATH"
...
# Fire!
MACHINE="$MACHINE" bitbake "$IMAGE"

나의 목표는 무작정 빌드를 돌리는 것이 아니라 쉘 환경에 진입하여 설정을 확인하고 제어하는 것이다. 따라서 entrypoint-build.sh에서 자동으로 빌드가 시작되는 것을 막고, Docker 컨테이너 내부의 쉘을 획득하는 방식으로 진행하려 한다. 먼저 entrypoint-build.sh를 수정하여 쉘이 종료되지 않게 하거나 단순 메시지만 출력하도록 한다. 그다음 Docker 이미지를 빌드한다.

$ cd meta-raspberrypi/.github/workflows/docker-images/yocto-builder
$ docker build -t my-rpi-builder .

이미지 빌드가 완료되면 호스트의 작업 디렉토리를 마운트하고 필요한 환경 변수를 설정하여 컨테이너를 실행한다. 이때 entrypoint-build.sh를 실행하되, 스크립트가 끝나도 쉘이 유지되도록 하거나 스크립트 실행 후 쉘로 진입하는 방식을 취해야 한다. 여기서는 편의상 스크립트 실행 후 쉘 진입을 위해 다음과 같이 수행한다.

$ docker run -it --rm \
    -v $(pwd):/work \
    --env BASE_REF=master \
    --env MACHINE=raspberrypi4 \
    --env DISTRO=poky \
    --env IMAGE=core-image-base \
    my-rpi-builder \
    /bin/bash

SSH 접속 시 환영 메시지 출력하기 (Custom Layer 추가)

앞서 Docker 컨테이너 쉘까지 진입했으니 이제 본격적으로 나만의 커스터마이징을 적용해 보자. 목표는 라즈베리파이에 SSH나 시리얼로 접속했을 때, 밋밋한 기본 메시지 대신 내가 설정한 환영 메시지(MOTD: Message Of The Day)를 띄우는 것이다. 이를 위해 Yocto의 레이어(Layer) 시스템을 활용해 본다.

가장 먼저 할 일은 소스 코드를 준비하고 빌드 환경을 세팅하는 것이다. 수정해 둔 entrypoint-build.sh를 실행하면 Poky와 meta-raspberrypi 등을 다운로드하고 레이어를 설정해 준다. 스크립트 실행이 끝나면 build 디렉토리가 생성되는데, source 명령어로 환경 변수를 로드해야 비로소 빌드 명령어를 사용할 수 있다.

$ cd meta-raspberrypi/.github/workflows/docker-images/yocto-builder
$ /entrypoint-build.sh
$ source poky/oe-init-build-env build

기존 코드를 직접 수정하는 것은 유지 보수 측면에서 권장하지 않으므로, meta-welcome이라는 새로운 레이어를 만들어 변경 사항을 관리하는 방식을 사용한다. bitbake-layers 명령어를 사용하면 손쉽게 레이어 템플릿을 만들고 빌드 설정(conf/bblayers.conf)에 추가할 수 있다.

$cd ..$ bitbake-layers create-layer meta-welcome
$ bitbake-layers add-layer meta-welcome

리눅스 시스템의 로그인 메시지는 주로 /etc/motd 파일에 저장되며, Yocto에서는 base-files 레시피가 이를 관장한다. 따라서 meta-welcome/recipes-core/base-files 경로를 생성하고, base-files_%.bbappend 파일을 작성하여 기존 설치 과정(do_install) 뒤에 우리의 로직을 덧붙여야 한다. do_install:append() 함수 내에서 echo 명령어를 이용해 원하는 텍스트를 ${D}${sysconfdir}/motd(이미지 상의 /etc/motd)에 덮어쓰도록 작성한다.

$ mkdir -p meta-welcome/recipes-core/base-files
$ vim meta-welcome/recipes-core/base-files/base-files_%.bbappend

Code Snippet

FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"

do_install:append() {
    # 기존 motd 파일을 덮어쓴다.
    echo "---------------------------------------------------" > ${D}${sysconfdir}/motd
    echo " Welcome to My Custom Raspberry Pi OS! "             >> ${D}${sysconfdir}/motd
    echo " Built with Yocto Project & Docker "                 >> ${D}${sysconfdir}/motd
    echo "---------------------------------------------------" >> ${D}${sysconfdir}/motd
}

모든 준비가 끝났으니 이미지를 빌드한다. 처음 빌드라면 시간이 꽤 소요될 것이다.

$ cd build
$ MACHINE=raspberrypi4 bitbake core-image-base

빌드가 성공하면 tmp/deploy/images/raspberrypi4/ 경로에 생성된 .wic.bz2 이미지를 SD 카드에 구워 확인할 수 있다. 라즈베리파이를 부팅하고 접속했을 때 위에에 설정한 문구가 뜬다면 성공이다. 이처럼 Yocto는 Docker 환경 위에서도 레이어 구조를 활용해 시스템의 설정 파일까지 체계적으로 제어할 수 있다. 처음 환경 구축이 다소 번거로울 수 있지만, 한번 갖춰두면 이보다 강력한 개발 환경은 없다.