みつきんのメモ

組み込みエンジニアです。Interface誌で「My オリジナルLinuxの作り方」連載中

Yocto イメージレシピの調査

はじめに

bitbakeでビルド可能なイメージについて調査する。

pokyのmetaレイヤに含まれるものは下記のようになっている。

イメージのレシピ ディレクト
build-appliance-image_15.0.0.bb recipes-core/images
core-image-base.bb recipes-core/images
core-image-minimal-dev.bb recipes-core/images
core-image-minimal-initramfs.bb recipes-core/images
core-image-minimal-mtdutils.bb recipes-core/images
core-image-minimal.bb recipes-core/images
core-image-ptest-all.bb recipes-core/images
core-image-ptest-fast.bb recipes-core/images
core-image-tiny-initramfs.bb recipes-core/images
core-image-sato-dev.bb recipes-sato/images
core-image-sato-sdk.bb recipes-sato/images
core-image-sato.bb recipes-sato/images
core-image-rt-sdk.bb recipes-rt/images
core-image-rt.bb recipes-rt/images
core-image-weston-sdk.bb recipes-graphics/images
core-image-weston.bb recipes-graphics/images
core-image-x11.bb recipes-graphics/images
core-image-full-cmdline.bb recipes-extended/images
core-image-kernel-dev.bb recipes-extended/images
core-image-testcontroller-initramfs.bb recipes-extended/images
core-image-testcontroller.bb recipes-extended/images

この内、いくつかをピックアップして調査する。

レシピの内容を確認

これらのレシピは下記のような変数に影響受けることが多い。

  • MACHINE_FEATURES
  • DISTRO_FEATURES
  • IMAGE_FEATURES

ここではこれらをまとめてFEATURES変数と呼ぶ。

core-image-minimal

最小構成のイメージ。ターゲットのブートに最低限必要なパッケージを含んでいる。

具体的には下記のように設定されている。

IMAGE_INSTALL = "packagegroup-core-boot ${CORE_IMAGE_EXTRA_INSTALL}"

packagegroup-core-bootが最低限ブートに必要なパッケージを含んでいる。 実際のpackagegroup-core-bootの中身は、bitbake実行時の変数の値によって異なるため単純に一覧にすることはできない。

CORE_IMAGE_EXTRA_INSTALLは任意にイメージに追加したいパッケージの変数となる。この変数はlocal.confでのみ記述することができる。 パッケージ追加にはよくlocal.confにIMAGE_INSTALL:appendを記載する例を見かけるが、CORE_IMAGE_EXTRA_INSTALLに設定された値も最終的にIMAGE_INSTALL変数に代入されるため、 結果的には同じような働きをする。

core-image-minimal-dev

core-image-minimalの内容に、下記の内容が追加される。

IMAGE_FEATURES += "dev-pkgs"

dev-pkgsについては下記のように説明されている。

# - dbg-pkgs            - debug symbol packages for all installed packages in the rootfs

core-image-minimal-initramfs

core-image-minimalとは全く異なる。パッケージのインストールに関わる部分は下記のようになっている。

INITRAMFS_SCRIPTS ?= "\
                      initramfs-framework-base \
                      initramfs-module-setup-live \
                      initramfs-module-udev \
                      initramfs-module-install \
                      initramfs-module-install-efi \
                     "

PACKAGE_INSTALL = "${INITRAMFS_SCRIPTS} ${VIRTUAL-RUNTIME_base-utils} udev base-passwd ${ROOTFS_BOOTSTRAP_INSTALL}"

このイメージはIMAGE_INSTALL変数の影響を受けない。

core-image-minimal-mtdutils

core-image-minimalに下記が追加される。

IMAGE_INSTALL += "mtd-utils"

IMAGE_INSTALLに対して+=を使用しているが、local.confではこの記述は許容されていないので注意が必要。

Using IMAGE_INSTALL with the += BitBake operator within the /conf/local.conf file or from within an image recipe is not recommended. Use of this operator in these ways can cause ordering issues. Since core-image.bbclass sets IMAGE_INSTALL to a default value using the ?= operator, using a += operation against IMAGE_INSTALL results in unexpected behavior when used within conf/local.conf. Furthermore, the same operation from within an image recipe may or may not succeed depending on the specific situation. In both these cases, the behavior is contrary to how most users expect the += operator to work.

core-image-full-cmdline

内容としてはcore-image-minimal + packagegroup-core-full-cmdline + αという感じだが、レシピの作りとしてはcore-image-minimalをベースにはしていない。 パッケージのインストールに関わる部分は下記のようになっている。

IMAGE_FEATURES += "splash ssh-server-openssh"

IMAGE_INSTALL = "\
    packagegroup-core-boot \
    packagegroup-core-full-cmdline \
    ${CORE_IMAGE_EXTRA_INSTALL} \
    "

core-image-base

コンソール主体のイメージのレシピの中で実はこれが分かりづらい。レシピ全体を示す。

SUMMARY = "A console-only image that fully supports the target device \
hardware."

IMAGE_FEATURES += "splash"

LICENSE = "MIT"

inherit core-image

このイメージはcore-image.bbclassがインストールしようとするものを全てインストールするようなものになっている。 MACHINE_FEATURESなどのFEATURES変数で設定した値が全て反映されるため、fully supports the target device hardware.という位置づけになっている。

逆にcore-image-minimalもcore-image-full-cmdlineもIMAGE_INSTALL変数に=で値を設定することによって、core-image.bbclassでインストールされるパッケージに対して制限をかけている。

core-image-x11

core-image-baseにx11-base関連のFEATURESが追加されている。

IMAGE_FEATURES += "splash package-management x11-base"

REQUIRED_DISTRO_FEATURES = "x11"

DISTRO_FEATURESで"x11"を要求している。

core-image-weston

core-image-baseにweston関連のFEATUREが追加されている。

IMAGE_FEATURES += "splash package-management ssh-server-dropbear hwcodecs weston"

CORE_IMAGE_BASE_INSTALL += "gtk+3-demo"
CORE_IMAGE_BASE_INSTALL += "${@bb.utils.contains('DISTRO_FEATURES', 'x11', 'weston-xwayland matchbox-terminal', '', d)}"

DISTRO_FEATURESx11が設定されている場合、weston-xwaylandとmatchbox-terminalが追加されるようになっている。 BSPによってはこの部分がトラブルを起こすことがある。下記のようにDISTRO_FEATURESからx11を削除する必要となるケースがある。

DISTRO_FEATURES:remove = "x11"

core-image-sato

core-image-baseにx11-sato関連のFEATURESが追加されている。

IMAGE_FEATURES += "splash package-management x11-base x11-sato ssh-server-dropbear hwcodecs"

FEATURES変数とpackagegroup

イメージのレシピでは一部を除いてIMAGE_INSTALLで直接パッケージを使用せずにFEATURES変数の有無で必要なパッケージが選択されるようになっている。 これはFEATURES変数とpackagegroupは紐付けられていて、FEATRUESを設定することで、その機能の実現に必要なパッケージを含むpackagegroupが選択されるようになっているためである。

core-image.bbclassに下記のように定義されている。

FEATURE_PACKAGES_weston = "packagegroup-core-weston"
FEATURE_PACKAGES_x11 = "packagegroup-core-x11"
FEATURE_PACKAGES_x11-base = "packagegroup-core-x11-base"
FEATURE_PACKAGES_x11-sato = "packagegroup-core-x11-sato"
FEATURE_PACKAGES_tools-debug = "packagegroup-core-tools-debug"
FEATURE_PACKAGES_eclipse-debug = "packagegroup-core-eclipse-debug"
FEATURE_PACKAGES_tools-profile = "packagegroup-core-tools-profile"
FEATURE_PACKAGES_tools-testapps = "packagegroup-core-tools-testapps"
FEATURE_PACKAGES_tools-sdk = "packagegroup-core-sdk packagegroup-core-standalone-sdk-target"
FEATURE_PACKAGES_nfs-server = "packagegroup-core-nfs-server"
FEATURE_PACKAGES_nfs-client = "packagegroup-core-nfs-client"
FEATURE_PACKAGES_ssh-server-dropbear = "packagegroup-core-ssh-dropbear"
FEATURE_PACKAGES_ssh-server-openssh = "packagegroup-core-ssh-openssh"

厳密にはFEATURES変数と1:1で定義されている訳ではないが、その関係性を推測することはできる。

基本的にはpackagegroup内ではパッケージ間の依存関係が考慮されており、 packagegroup単位での追加、削除を行う場合には、パッケージ間の依存関係でトラブルになる可能性は低いであろうことが予想できる。

まとめ

pokyでビルド可能なイメージのレシピのうち下記のものを調査した。

  • core-image-minimal
  • core-image-minimal-dev
  • core-image-minimal-initramfs
  • core-image-minimal-mtdutils
  • core-image-full-cmdline
  • core-image-base
  • core-image-x11
  • core-image-weston
  • core-image-sato

それぞれのイメージがどのようにパッケージを選択しているのかなど、ざっくりと理解できた。 FEATURES変数により選択されるパッケージが変化するため、IMAGE_INSTALL変数に直接パッケージを羅列している訳ではないということがわかった。 core-image-minimal-initramfsについては名前に反してcore-image-minimalはベースにしておらず、IMAGE_INSTALL変数が作用しないこともわかった。

基本的にはコンソール環境では、core-image-baseを選択するのが良さそう。 core-image-x11やcore-image-westonなどのGUIを含むイメージはcore-image-baseをベースとしている。

YoctoProject wic入門

はじめに

YoctoProjectではbitbakeで生成された成果物をもとに、SDカードなどに直接書き込むことができるディスクイメージを作成するため、 wicというコマンドを提供している。ヘルプにwicは次のようなものだという記載がある。

The 'wic' command generates partitioned images from existing OpenEmbedded build artifacts.

ここでいうOpenEmbedded build artifactsとはbitbakeで生成されたもののことで、下記のようなものを指す。

Image generation is driven by partitioning commands contained in an 'Openembedded kickstart' (.wks) file (see 'wic help kickstart')

イメージはwksファイルに記述されているパーティション情報に基づいて生成される。

the functionality of those scripts is implemented by a general-purpose partitioning 'language' based on Red Hat kickstart syntax

Red Hat kickstartの構文をベースにしているとのこと。

YoctoProject LICHEE RV DOCKを動かすでwksを調べる必要があったので、この際入門してみる。今回はサンプルとして国内に流通していないであろうLICHEE RV DOCKではなく、Raspberry Pi4を扱う。

meta-raspberrypiとwic

wksファイル

wicコマンドは、パーティション構成が定義されているwksファイルを参照する。meta-raspberrypiにはwic/sdimage-raspberrypi.wksというwksファイルが含まれている。

sdimage-raspberrypi.wksは次のようになっている。

# short-description: Create Raspberry Pi SD card image
# long-description: Creates a partitioned SD card image for use with
# Raspberry Pi. Boot files are located in the first vfat partition.

part /boot --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --active --align 4096 --size 20
part / --source rootfs --ondisk mmcblk0 --fstype=ext4 --label root --align 4096

このwksからは下記のようなパーティション構成のイメージが作成される。

No マウントポイント FSタイプ ラベル サイズ その他
1 /boot vfat boot 20MiB active
2 / ext4 root

1つ目のパーティションにはactiveの指定がついている。アクティブパーティションによると、この領域からOSが起動される。

wicイメージ

bitbakeを実行するとRaspberry Pi向けのwicイメージが作成されるが、どのようにしてsdimage-raspberrypi.wksが参照されるかを確認する。

$ grep -r 'sdimage-raspberrypi.wks' ./poky/meta-raspberrypi
./conf/machine/include/rpi-base.inc:WKS_FILE ?= "sdimage-raspberrypi.wks"

実装の詳細までは調査していないのだが、IMAGE_FSTYPES変数にwicが設定されている場合にdo_image_wicタスクが実行され、 WKS_FILE変数で指定されたwksファイルがwicコマンドに渡されるという流れになっていると推測される。

meta-raspberrypiのWKS_FILE変数は?=で代入されているためlocal.confで上書き可能となっている。

WKS_FILEに指定されるwksファイルは、レイヤのwicディレクトリに配置されることを期待している。 meta-raspberrypiの場合はsdimage-raspberrypi.wksmeta-raspberrypi/wicに配置されている。

これらのことから自前のwksを適用するためには、下記の手順が必要になることがわかる。

  1. 自作のレイヤにwicディレクトリを用意する
  2. wicディレクトリにwksファイルを作成する
  3. WKS_FILEにwksを設定する

wicコマンドの実行

bitbakeと同様にoe-init-build-envで環境設定を実行することで、wicコマンドを単体で実行することができる。 その場合は、任意のwksファイルを指定することができる。

下記のような内容でtest.wksファイルを作成してみる。

include sdimage-raspberrypi.wks
part /opt --ondisk mmcblk0 --fstype=ext4 --label opt --align 4096 --size 512

wksファイルの記述に関しては後述するが、このwksファイルではsdimage-raspberrypi.wksの内容に加え、新たに512MiBのパーティションを作成し、 /optにマウントされるようにする。ただしマウントポイントは自動的に作成されないため、予めrootfs上にディレクトリを作成しておく必要がある。

part --fstype=ext4 --label opt --align 4096 --size 512

下記のように実行する。

$ wic create ./test.wks -e core-image-base -o output

wicコマンドはビルド済みの成果物を参照するため、先にbitbakeでイメージをビルドしておく必要がある(この例ではcore-image-base)。

outputディレクトリに下記のものが生成されている。

ファイル 内容
debugfs_script
fstab wksの内容を反映するためのfstab
test-202205280913-mmcblk0.direct ディスクイメージ
test-202205280913-mmcblk0.direct.p1 第1パーティションのイメージ
test-202205280913-mmcblk0.direct.p2 第2パーティションのイメージ
test-202205280913-mmcblk0.direct.p3 第3パーティションのイメージ

test-202205280913-mmcblk0.directが生成されたwicイメージで、それ以外のファイルは中間生成物となる。

fstabはルートFSの/etc/fstabと差し替えることで、wksで記述されたパーティション構成をルートFSに反映する。 fstabの差し替えを行いたくない場合には--no-fstab-updateをwicコマンドに渡す。

fstab

wicコマンドで生成されたfstabの内容を見てみる。

# stock fstab - you probably want to override this with a machine specific one

/dev/root            /                    auto       defaults              1  1
proc                 /proc                proc       defaults              0  0
devpts               /dev/pts             devpts     mode=0620,ptmxmode=0666,gid=5      0  0
tmpfs                /run                 tmpfs      mode=0755,nodev,nosuid,strictatime 0  0
tmpfs                /var/volatile        tmpfs      defaults              0  0

# uncomment this if your device has a SD/MMC/Transflash slot
#/dev/mmcblk0p1       /media/card          auto       defaults,sync,noauto  0  0

/dev/mmcblk0p1  /boot   vfat    defaults    0  0
/dev/mmcblk0p3  /opt    ext4    defaults    0  0

/dev/rootカーネルパラメータのroot=に指定されたものを指す。 ファイルシステム上に/dev/root存在しない点に注意。

動作モード

wicコマンドには次の2つの動作モードがある。

  • Raw mode
  • Cooked mode

wicコマンドは内部的に下記のディレクトリを示す変数を参照する。

変数 ディレクトリの概要
ROOTFS_DIR ルートFSを含むディレクトリ(/rootfs)
BOOTIMG_DIR ブート関連ファイルを含むディレクトリ(/EFI、/syslinuxなど)
KERNEL_DIR カーネルを含むディレクト
NATIVE_SYSROOT ホスト上で実行するツールを含むディレクト

Raw modeはこれらの変数を全て手動で指定するモードで、自由度が高い代わりに若干手間がかかる。 Cooked modeは-eで指定したイメージ名から自動的にこれらの変数を設定するモードとなる。

基本的にはCooced modeを使用することになる。

wksファイルの内容

8 OpenEmbedded Kickstart (.wks) Referenceによると、パーティションコマンドは、本家のkickstartが持っている機能のうちの partition(part)とbootloaderしか実装されていない。

先述のsdimage-raspberrypi.wksではpartコマンドしか使用されていない。

bootloaderコマンド

あまり使用されないの省略する。

partitionコマンド

実のところ、partitionコマンドの基本的な構文は下記のバリエーションしかない。

part [moutnpoint]
partition [mountpoint]

[mountpoint]は省略可能で/pathの形式以外ではswapが使用可能。 swapはそのままスワップとして使用される領域となる。

それ以外のオプションについては8.2 Command: part or partitionを参照のこと。

--sourceについて

--sourceオプションは若干特殊となっている。下記の例を取り上げる。

part /boot --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --active --align 4096 --size 20
part / --source rootfs --ondisk mmcblk0 --fstype=ext4 --label root --align 4096

これらはパーティションにどのようなファイルを含めるかを指定する。--source rootfsを指定した場合はそのパーティションにルートFSが含まれる。

--source bootimg-partitionを指定した場合は、IMAGE_BOOT_FILES変数に指定されたファイルが含まれる。IMAGE_BOOT_FILESにはブートに必要なファイル群が指定される。

このsourceは単なるオプションではなく、実際にはpluginの指定となる。つまり、各パーティションへのコピー処理はpluginとして実装されていることになる。

これらのプラグインはSource pluginと呼ばれ、poky/scripts/lib/wic/plugins/sourceに格納されているpythonスクリプトで実装されている。

標準では下記のプラグインが提供されている。

  • bootimg-biosplusefi.py
  • bootimg-efi.py
  • bootimg-partition.py
  • bootimg-pcbios.py
  • empty.py
  • isoimage-isohybrid.py
  • rawcopy.py
  • rootfs.py

partコマンドで指定可能とされるオプションの一部は、実行されるSource pluginによっては作用しない。例えば--source rawcopyの場合は対象のパーティションファイルシステムを作成しないため--fstypeは作用しない。 Source pluginの処理内容はそれぞれソースコードを確認するのが一番確実だと考える。

Sourcde pluginは必要に応じて自分で実装することもできる。その際は自分のレイヤのscripts/lib/wic/plugins/source/もしくはlib/wic/plugins/sourceに配置しておくと、wicコマンドがプラグインとして認識することができる。

Source pluginの実装について

先述の通りSource pluginは必要に応じて自分で実装することができる。

Source pluginの条件は下記の通り。

  1. SourcePluginクラスのサブクラスとして実装されている
  2. name変数にユニークな名前が設定されている

まずこの条件を満たせばプラグインとして認識される。

Source pluginはTemplate Methodパターンで実装されているようで、下記のクラスメソッドをサブクラスで一部もしくは全部を実装することで機能を実現する。

メソッド 機能
do_configure_partition ブートローダの設定ファイルの生成など
do_stage_partition 通常は空実装
do_prepare_partition パーティションへのファイルのコピー
do_post_partition パーティションへの署名など
do_install_disk MBRへの書き込みなど

Source pluginの本体は、コピーを実施するdo_prepare_partitionと言っても過言ではない。 どのSource pluginもdo_prepare_partitionは実装することになる。 それ以外のメソッドはブートローダに関連する処理が多く、ブートローダに関連しない場合は実装されないことがある。

これらのクラスメソッドは下記の順序で呼び出される。

この図ではSource pluginで実装されるメソッドの呼び出し順序のざっくりとしたイメージがつかめれば良い。

Source pluginを実装する

KERNEL_DIRに存在するファイルをパーティションにコピーするだけのSource pluginを実装してみる。 rawcopyとは異なり、指定のファイルをファイルシステム上に単純にコピーする。

書式には下記のようになる。

part barebox --source simplecopy --sourceparams="file=vmlinuz" --ondisk mmcblk --fstype=vfat

--sourceparamsの部分をfiles=にしてスペース区切りで複数のファイルをコピーすることもできる。

rootfsプラグインをベースにdo_prepare_partitionメソッドのみ実装している。コピー語のファイルのオーナーに整合性を持たせるために、 __get_pseudoをまるっと借用している。

コメント含めて69行程度でSource pluginが実装できた。

まとめ

wicについて調査した。

wicはRed Hat kickstartをベースにしたwksファイルをもとにパーティション情報を持ったディスクイメージファイルを生成する。 bitbakeで生成されたファイルを扱うことに特化しており、本家のkickstartと比較すると機能は限定的である。

wicはコマンド単体でも実行可能となっており、bitbakeでイメージを生成したあとであれば独自のwksファイルで独自の構成のイメージファイルを生成することも可能となっている。

SourcePluginクラスを継承し、必要なメソッドを実装することで独自のSource pluginを追加することも可能。

wicコマンドのhelpが充実しているので、よく読んでみるといろいろと知らなかった機能が発見できたりする。

YoctoProject LICHEE RV DOCKを動かす

はじめに

SiPEED LICHEE RV DOCKが転がっている。

これにはAllwinner D1が搭載されている。meta-riscvのマシン定義にnezha-allwinner-d1があるので試してみる。

環境を構築する

作業環境

$ mkdir -p ~/work/yocto/nezha-kirkstone
$ cd ~/work/yocto/nezha-kirkstone

CROPSの使用

run_crops.shをダウンロードして使用する。

$ wget https://gist.githubusercontent.com/mickey-happygolucky/2b9671f062de558f3312f7378ed3c240/raw/ab57346aa3911ff4219ac048f5b24519249aec17/run_crops.sh
$ chmod +x ./run_crops.sh

CROPSについてはYocto Project CROPSを使ってみる参照。

pokyの取得

$ git clone -b kirkstone git://git.yoctoproject.org/poky.git

環境変数の設定

$ ./run_crops.sh

meta-riscvの取得

$ ./run_crops.sh bitbake-layers layerindex-fetch meta-riscv

local.confを編集

MACHINE = "nezha-allwinner-d1"
DL_DIR = "${TOPDIR}/../downloads"

bitbake実行

$ ./run_crops.sh bitbake core-image-base

SDカードへの書き込み

/dev/sdXは実際のデバイスで読み替えること。

$ bmaptool copy build/tmp/deploy/images/nezha-allwinner-d1/core-image-base-nezha-allwinner-d1.wic.gz /dev/sdX

16GB以上のサイズのSDカードが必要なので注意。

ボード起動

Sipeed Lichee RVを参考にUSB-シリアル変換ケーブルでPCに接続し、minicomなどの端末を起動する。

LICHEE RV DOCKにSDカードを挿入し電源を投入する。

root@nezha-allwinner-d1:~# uname -a
Linux nezha-allwinner-d1 5.16.0-nezha #1 PREEMPT Sun Jan 16 18:33:32 UTC 2022 riscv64 GNU/Linux

Linux起動はできるようだ。

イメージのサイズが大きい理由

bmaptool実行時の出力をみると下記のような表示が見つかる。

bmaptool: info: 3605504 blocks of size 4096 (13.8 GiB), mapped 23255 blocks (90.8 MiB or 0.6%)

なぜか13.8GiBのイメージとなっている。本来サイズがそれほど大きくならないはずのcore-image-baseなのになぜなのだろうか。

bitbakeではイメージファイルを出力するためにwicを使用している。 wicはkickstartをベースとしていて、パーティション設定済みのディスクイメージを出力することができる。

wicはパーティション構成を定義するwksファイルを参照している。 bitbake実行時にどのwksを参照するかはWKS_FILEで設定される。

$ ./run_crops.sh bitbake core-image-base -e | grep '^WKS_FILE='
WKS_FILE="nezha.wks"

nezha.wksが参照されていることがわかった。 meta-riscvでnezha.wksを探してみる。

$ find poky/meta-riscv -name nezha.wks
poky/meta-riscv/wic/nezha.wks

nezha.wksが見つかったので内容を確認する。

# short-description: Create SD card image for Nezha Allwinner D1 development board

part boot0 --source rawcopy --sourceparams="file=boot0_sdcard_sun20iw1p1.bin" --offset 16s --ondisk mmcblk0 --no-table

part u-boot --source rawcopy --sourceparams="file=u-boot.toc1" --offset 32800s --ondisk mmcblk0 --no-table

part /boot --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --align 4096 --fixed-size 64 --active

part /     --source rootfs --ondisk mmcblk0 --fstype=ext4 --label rootfs  --align 4096 --fixed-size 14000 --exclude-path data/

--fixed-size 14000の指定がある。fixed-sizeのデフォルトの単位はMiBなので、約14GBとなっている。

これが原因だった。

まとめ

SiPEED LICHEE RV DOCK向けにpokyを作った。 とりあえず起動はできるようだ。

wksの設定によってwicイメージが14GiB固定になっていた。

Yocto Project Kirkstone(4.0) タスクのネットワークアクセス制限の実現方法

はじめに

前回で、Kirkstoneではdo_fetch以外のタスクからは原則ネットワークアクセスができなくなっていることについて調査した。

Release 4.0 (kirkstone)には下記のように記載があり、カーネル機能を使用しているとのことだったが、具体的にどのような機能が使用されているかはドキュメントには見当たらなかった。

Network access from tasks is now disabled by default on kernels which support this feature (on most recent distros such as CentOS 8 and Debian 11 onwards). This means that tasks accessing the network need to be marked as such with the network flag.

興味が湧いたので調べてみた。

bitbakeの実装

該当箇所の特定

poky/bitbakeディレクトリ配下でnetworkgrepしてみる。するとbitbake-workerというスクリプトに怪しい行を見つけた。

$ grep -n -r 'network' .
... (snip) ...
./bin/bitbake-worker:264:                if not the_data.getVarFlag(taskname, 'network', False):
... (snip) ...

この行事体は、tasknameで指定されたタスクにnetworkというフラグが立っているかをチェックしているだけだが、 この付近でネットワークアクセスをチェックしているということが推測できる。

if not the_data.getVarFlag(taskname, 'network', False):
    if bb.utils.is_local_uid(uid):
        logger.debug("Attempting to disable network for %s" % taskname)
        bb.utils.disable_network(uid, gid)
    else:
        logger.debug("Skipping disable network for %s since %s is not a local uid." % (taskname, uid))

bb.utils.disable_network(uid, gid)が怪しい。

bb.utils.disable_networkの実装

これが本体と仮定して実装を眺めてみる。この関数は./lib/bb/utils.pyで実装されている。

def disable_network(uid=None, gid=None):
    """
    Disable networking in the current process if the kernel supports it, else
    just return after logging to debug. To do this we need to create a new user
    namespace, then map back to the original uid/gid.
    """
    libc = ctypes.CDLL('libc.so.6')

    # From sched.h
    # New user namespace
    CLONE_NEWUSER = 0x10000000
    # New network namespace
    CLONE_NEWNET = 0x40000000

    if uid is None:
        uid = os.getuid()
    if gid is None:
        gid = os.getgid()

    ret = libc.unshare(CLONE_NEWNET | CLONE_NEWUSER)
    if ret != 0:
        logger.debug("System doesn't suport disabling network without admin privs")
        return
    with open("/proc/self/uid_map", "w") as f:
        f.write("%s %s 1" % (uid, uid))
    with open("/proc/self/setgroups", "w") as f:
        f.write("deny")
    with open("/proc/self/gid_map", "w") as f:
        f.write("%s %s 1" % (gid, gid))

処理の流れやログから判断すると、この部分がネットワーク制限および、カーネルがサポートしているかをチェックしている部分になる。

    ret = libc.unshare(CLONE_NEWNET | CLONE_NEWUSER)
    if ret != 0:
        logger.debug("System doesn't suport disabling network without admin privs")
        return

libcのunshare関数を呼び出している。

仕組みとしてはタスクを実行するのプロセスのユーザーとネットワークについて名前空間を分離するということをしているようだ。 詳しいわけではないのでざっくりの理解だが、 名前空間を分離することで、分離されたネットワークに対してアクセスすることになるため、 プログラムからは見かけ上、正常にネットワークアクセスできるが、実際には外にパケットが飛んでいかないため、接続エラーになるようなイメージ。

実際に前回試した時のエラーが下記のような感じとなっている。

| Cloning into 'helloworld'...
| fatal: unable to access 'https://github.com/mickey-happygolucky/helloworld.git/': Could not resolve host: github.com
| Cloning into 'helloworld'...
| fatal: unable to access 'https://github.com/mickey-happygolucky/helloworld.git/': Could not resolve host: github.com
| Cloning into 'helloworld'...
| fatal: unable to access 'https://github.com/mickey-happygolucky/helloworld.git/': Could not resolve host: github.com
| -- Had to git clone more than once:
|           3 times.
| CMake Error at /home/mickey/work/yocto/kirkstone/build/tmp/work/core2-64-poky-linux/example/0.1-r0/build/helloworld/tmp/helloworld-gitclone.cmake:31 (message):
|   Failed to clone repository:
|   'https://github.com/mickey-happygolucky/helloworld.git'
(...snip...)
Summary: 1 task failed:
  /home/mickey/work/yocto/kirkstone/build/meta-test/recipes-example/example/example_0.1.bb:do_compile

おそらくその理解で問題ないだろう。

使用されるカーネル機能

libc.unshare(CLONE_NEWNET | CLONE_NEWUSER)によってどのようなカーネル機能が必要となるかを調べる。

google先生に聞いたらドンピシャですごいテストコードが出てきた。

# Requires at least kernel configuration options:
#   CONFIG_NAMESPACES=y
#   CONFIG_NET_NS=y
#   CONFIG_UTS_NS=y
def IfPossibleEnterNewNetworkNamespace():
  """Instantiate and transition into a fresh new network namespace if possible."""
  sys.stdout.write('Creating clean namespace... ')
  try:
    UnShare(CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWNET)
  except OSError as err:
    print('failed: %s (likely: no privs or lack of kernel support).' % err)
    return False

UnShareもbitbakeのコードのlibc.unshare(CLONE_NEWNET | CLONE_NEWUSER)と同じようにlibcのunshare関数を呼び出すだけのものと推測できる。

コメントのカーネルコンフィグと、UnShareに渡しているフラグはおそらく1:1になっているので、CLONE_NEWNETに対応するカーネル機能はCONFIG_NET_NS=yと読み取ることができる。

Ubuntuカーネルの対応状況

Ubuntu 20.04のカーネルはこの機能に対応しているのか確認してみる。

$ grep 'CONFIG_NET_NS' /boot/config-$(uname -r)
CONFIG_NET_NS=y
CONFIG_NET_NSH=m

対応していることがわかる。

既に修正済みだが前回の記事で、下記のように記載してしまった。

Ubuntu 20.04の環境では、全く問題なくビルドできてしまった。

これは実際には間違いで、Ubuntu 20.04 + CROPS の環境では問題なくビルドができたということだった。

CROPSはDockerコンテナを使用しているということが大きな違いとなる。

なぜUbuntu 20.04カーネルを使用していてもCROPS経由だとこの機能をすり抜けたのか。これはunshareに原因がある。

Dockerコンテナは既に名前空間が分離された状態になるため、コンテナ上でunshareを実行した場合、ホスト上で実行した場合と結果が変わることがある。(筆者は変わらないケースを確認していないが。)

ホスト上とコンテナ上でのunshareの挙動の違い

まず下記のようなプログラムを作成して挙動を確認する。

#include <iostream>
#include <sched.h>

int main() {
    auto ret = ::unshare(CLONE_NEWNET|CLONE_NEWUSER);
    std::cout << "unshare: ret=" << ret << std::endl;
    return 0;
}

main.cppとして保存して下記のようにコンパイルする。

$ g++ ./main.cpp -O -o call_unshare

ホスト上での実行結果

$ ./call_unshare 
unshare: ret = 0

問題なく0(成功)が返る。

Dockerコンテナ上での実行

$ docker run --rm -it -v "$(pwd):/work" ubuntu:20.04 bash -c '/work/call_unshare'
unshare: ret = -1

想定した通りに-1(失敗)が返る。

bitbakeは下記の処理によって、コンテナ上で実行される場合、システムが機能をサポートしていないとみなしてしまう。

    ret = libc.unshare(CLONE_NEWNET | CLONE_NEWUSER)
    if ret != 0:
        logger.debug("System doesn't suport disabling network without admin privs")
        return

ある意味抜け穴。

まとめ

  • Kirkstoneで実装されたタスクのネットワークアクセス制限の実装を調べた。
  • unshare(CLONE_NEWNET | CLONE_NEWUSER)によってネットワークの名前空間を分離することで実現していることがわかった。
  • Ubuntu 20.04 + CROPSでこのチェックが機能しなかった原因はDockerコンテナ上でunshareが失敗するため。
  • コンテナ上ではネットワークアクセス制限のチェックが素通りしてしまうことがわかった。

Yocto Project Kirkstone(4.0) do_fetch以外でのネットワークアクセスについて

はじめに

GW前にYoctoProject 4.0(kirkstone)がリリースされた。

変更点の詳細はRelease 4.0 (kirkstone)を参照。

使ってみようと思っていたら、ツイッターで下記のような情報が流れてきた。

Release 4.0 (kirkstone)に下記のような記述を見つけた。

Network access from tasks is now disabled by default on kernels which support this feature (on most recent distros such as CentOS 8 and Debian 11 onwards). This means that tasks accessing the network need to be marked as such with the network flag.

関係ありそうなのでちょっと実験をしてみる。

Kirkstoneをビルド

作業場所は~/work/yocto/kirkstoneとする。

$ mkdir -p ~/work/yocto/kirkstone && cd ~/work/yocto/kirkstone

ソースの取得

下記のコマンドでpokyのビルド環境をダウンロードする。

$ git clone -b kirkstone git://git.yoctoproject.org/poky.git

観葉変数の設定

$ source poky/oe-init-build-env

ここまでで実験の準備は整った。

cmakeのexternal_projectの実験

タスクの中からネットワークを参照するとエラーになるかどうかを確認する。

テスト用レイヤの作成

$ bitbake-layers create-layer meta-test
$ bitbake-layers add-layer ./meta-test

レシピの作成

作成されたレイヤにデフォルトで含まれているrecipes-example/example/example_0.1.bbを使用する。

ソースファイルを配置するディレクトリを作成し、CMakeLists.txtを作成する。

$ mkdir ./build/meta-test/recipes-example/example/files
$ vim ./build/meta-test/recipes-example/example/files/CMakeLists.txt

CMakeLists.txtの内容は下記のようにする。

cmake_minimum_required(VERSION 3.16)
project(example)

set(helloworld_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/helloworld")
set(helloworld_INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}/helloworld")
set(helloworld_CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${helloworld_INSTALL_DIR})

include(ExternalProject)
ExternalProject_Add(
  helloworld
  PREFIX ${helloworld_PREFIX}
  GIT_REPOSITORY https://github.com/mickey-happygolucky/helloworld.git
  GIT_TAG main
  INSTALL_DIR ${helloworld_INSTALL_DIR}
  CMAKE_ARGS ${helloworld_CMAKE_ARGS}
)

これはExternal Projectとしてgithubからhelloworldを取得するようになっている。

example_0.1.bbを編集する。

$ vim ./build/meta-test/recipes-example/example/example_0.1.bb

内容は下記。

SUMMARY = "bitbake-layers recipe"
DESCRIPTION = "Recipe created by bitbake-layers"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

inherit cmake

SRC_URI = "file://CMakeLists.txt"

S = "${WORKDIR}"

do_install() {
    install -d ${D}/${bindir}
    install ${B}/helloworld/bin/helloworld ${D}/${bindir}
}

exampleのビルド

exampleをビルドしてみる。

$ bitbake example

Ubuntu20.04 + CROPSではエラーにならない

Ubuntu 20.04のCROPS環境では、全く問題なくビルドできてしまった。

debian 11ではエラー

下記のような記述があったので、VMdebian 11 の環境を作成して試してみた。(すごい時間がかかった。)

Network access from tasks is now disabled by default on kernels which support this feature (on most recent distros such as CentOS 8 and Debian 11 onwards).

するとexampleのレシピが、helloworldをクローンするタイミングでエラーが発生した。

| Cloning into 'helloworld'...
| fatal: unable to access 'https://github.com/mickey-happygolucky/helloworld.git/': Could not resolve host: github.com
| Cloning into 'helloworld'...
| fatal: unable to access 'https://github.com/mickey-happygolucky/helloworld.git/': Could not resolve host: github.com
| Cloning into 'helloworld'...
| fatal: unable to access 'https://github.com/mickey-happygolucky/helloworld.git/': Could not resolve host: github.com
| -- Had to git clone more than once:
|           3 times.
| CMake Error at /home/mickey/work/yocto/kirkstone/build/tmp/work/core2-64-poky-linux/example/0.1-r0/build/helloworld/tmp/helloworld-gitclone.cmake:31 (message):
|   Failed to clone repository:
|   'https://github.com/mickey-happygolucky/helloworld.git'
(...snip...)
Summary: 1 task failed:
  /home/mickey/work/yocto/kirkstone/build/meta-test/recipes-example/example/example_0.1.bb:do_compile

たぶんこれが件の現象と同じものだと思う。このケースではdo_compileタスクがエラーを起こしているので、下記をexample_0.1.bbに追加する。

do_compile[network] = "1"

これでビルドがとおるようになった。

まとめ

  • kirkstoneでは、fetch以外のタスクがネットワークにアクセスするとエラーがで発生するようになった。
  • ただしホストOSがUbuntu 20.04の場合では発生せず、debian 11やCentOS8など新し目のディストロだと発生するようだ。
  • 基本的にはdo_fetch以外のタスクがネットワークにアクセスしては行けない。
  • その他のタスクでネットワークアクセスしたい場合はタスクにnetworkフラグを設定することで許容される。

※この記事の発端となったツイートの方はまだ解決していないようなので、まだ調査の余地はあるかも。

pandocのdockerでEisvogelを使用する

はじめに

pandocが提供しているDockerコンテナpandoc/latexをベースにEisvogelを使用できる環境を作成する。

目指すゴールは下記が全て機能すること。

  • PlantUMLが使用できる
  • Eisvogelを使用してきれいなPDFを出力できる
  • pandoc-crossrefが使用できる

[2022/4/9 追記]
成果物をgithubに置いた

PlantUMLを使用できる環境の作成

pandoc/coreをベースにPlantUMLを使用できる環境はDockerを使用して比較的新しいpandoc環境を簡単に構築するで構築した。

pandoc/coreは標準でpandoc-crossrefが使用可能となっている。

今回使用するpandoc/latexはpandoc/coreをベースにしているため、pandoc/coreを使用して実現できた機能はそのまま転用することができる。

Eisvogelを使用できる環境の作成

TeX関連のパッケージの追加

執筆時点のpandoc/latexではTexLive 2021をベースとしLaTeX環境が構築されているため、tlmgrでLaTeX用のパッケージをインストールできる環境が整っている。

Eisvogelのサイトで案内されている手動でのインストールが必要があるパッケージは下記のようになっている。

adjustbox babel-german background bidi collectbox csquotes everypage filehook
footmisc footnotebackref framed fvextra letltxmacro ly1 mdframed mweights
needspace pagecolor sourcecodepro sourcesanspro titling ucharcat ulem
unicode-math upquote xecjk xurl zref

これらをDockerイメージに追加する必要がある。

また、日本語を取り扱う必要があるので、この辺りのパッケージも必要になる気がする。

babel-japanese luatexja haranoaji

Eisvigolがbabel-germanを使用しているのでbabel-japanese。pandocのpdfengineにlualatexを指定する際に日本語を取り扱うためluatexja。

日本語のフォントであるharanoajiを追加する。

scrartcl.clsのエラー

環境構築後にEisvigolを使用してpandocを実行すると下記のようなエラーが発生した。

Error producing PDF.
! LaTeX Error: File `scrartcl.cls' not found.

Type X to quit or <RETURN> to proceed,
or enter new name. (Default extension: cls)

Enter file name: 
! Emergency stop.
<read *> 
         
l.54 \usepackage

Where to find scrartcl.cls ? #238に、koma-scriptパッケージを追加すると良さそうなことが書いてあるので、tlmgrでkoma-scriptを追加する。

Eisvogelのインストール

github上のソースコードにraw指定してhttps://raw.githubusercontent.com/Wandmalfarbe/pandoc-latex-template/master/eisvogel.texのようなURLからファイルを取得するようなサイトをいくつか見かけたが、ここはお行儀よくきちんとリリース物を取得するようにしておく。

/opt/pandoc/templates以下にpandoc-latex-template本体となるファイルを配置する。

mkdir -p /opt/pandoc/templates
wget -O - https://github.com/Wandmalfarbe/pandoc-latex-template/releases/download/v2.0.0/Eisvogel-2.0.0.tar.gz | tar zxvf - -C /opt/pandoc/templates

pandoc-crossrefを使用できる環境を作成

pandoc/coreをベースとしているpandoc/latexの環境ではpandoc-crossrefを使用すること自体はできる。 ただし、Eisvogelと併用しようとすると、いろいろとバッドノウハウが必要になる。

筆者が遭遇した問題を挙げると下記のような問題の対処が必要となる。

  • pandoc-crossref使用時にlistingを有効化するとエラーになる
  • リストのキャプションが「Listing」に固定される

pandoc-crossref使用時にlistingを有効化するとエラーになるの対処

pandoc-crossref使用時にlistingを有効化すると下記のようなエラーになる。

Error producing PDF.
! LaTeX Error: Environment lstlisting undefined.

See the LaTeX manual or LaTeX Companion for explanation.
Type  H <return>  for immediate help.
 ...

l.456 \begin{lstlisting}

この辺はpandoc-crossrefとEisvogelを併用するにも回避方法を記載している。

最終的にはpandoc実行時に-M listingsとしてメタデータを定義してやると回避できる。

リストのキャプションが「Listing」に固定されることを回避する

pandoc-crossrefでは、図表番号のキャプションにつくprefixをyamlで定義することができる。このファイルはcrossref_config.yamlとして作成しておく。

figureTitle: "図 "
tableTitle: "表 "
listingTitle: "リスト "
figPrefix: "図"
eqnPrefix: "式"
tblPrefix: "表"
lstPrefix: "リスト"

しかし、listingを有効化すると、リストのキャプションのprefixがListingで固定されてしまう。 せっかくpandoc-crossrefを使用するのでyamlで定義したものと一致させたい。

これを変更するにはheader-includesで下記のようにしてLaTeXの変数を上書きする必要がある。詳しいやり方は大学のレポートをMARKDOWNで爆速で書く話を参照。

header-includes:
- \renewcommand{\lstlistingname}{Source Code}

しかし、マークダウンに毎回yamlヘッダで指定するのは嫌なので、下記の様にtexファイルを作成しpandoc実行時に-Hオプションで指定する。このファイルはheader.texとして作成しておく。

\renewcommand{\lstlistingname}{リスト}

環境構築

ここまでの内容を踏まえてDockerfileを作成し、そのコンテナを使用してpandocを実行するためのコマンドラインを考える。

Dockerfile

Dockerfileは下記のようになった。

FROM pandoc/latex

# Eisvogel latex-template
RUN tlmgr install \
    adjustbox \
    babel-german \
    background \
    bidi \
    collectbox \
    csquotes \
    everypage \
    filehook \
    footmisc \
    footnotebackref \
    framed \
    fvextra \
    letltxmacro \
    ly1 \
    mdframed \
    mweights \
    needspace \
    pagecolor \
    sourcecodepro \
    sourcesanspro \ 
    titling \
    ucharcat \
    ulem unicode-math \
    upquote \
    xecjk \
    xurl \
    zref \
    koma-script

# Japanese
RUN tlmgr install \
    babel-japanese \
    luatexja \
    haranoaji

# Eisvogel
RUN mkdir -p /opt/pandoc/templates && \
    wget -O - https://github.com/Wandmalfarbe/pandoc-latex-template/releases/download/v2.0.0/Eisvogel-2.0.0.tar.gz | \
    tar zxvf - -C /opt/pandoc/templates

RUN apk add --no-cache \
    graphviz \
    openjdk11 \
    python3 \
    py3-pip \
    ttf-droid \
    ttf-droid-nonlatin

RUN apk add --no-cache \
    wkhtmltopdf

ARG plantuml_version="1.2022.2"
RUN wget https://github.com/plantuml/plantuml/releases/download/v${plantuml_version}/plantuml-${plantuml_version}.jar -O /opt/plantuml.jar

RUN pip3 install --upgrade pip
RUN pip3 install pandoc-plantuml-filter

ENV PLANTUML_BIN="java -jar /opt/plantuml.jar"

ENTRYPOINT [ "/usr/local/bin/pandoc" ]

これを下記のようにビルドしてイメージを作成する。

$ docker build -t pandoc-eisvogel .

dockerコンテナを利用してpandocでPDFを生成する

pandoc-eisvogelというタグでイメージを作成したので、これを利用してコンテナを実行する。

マークダウンファイル

対象となるマークダウンファイルをREADME.mdとして作成する。

---
title: 使用例
subtitle: サブタイトル
date: 2022-04-03
author: jhon doo
---

# 概要

概要の本文

## 見出し2

見出し2の本文


# PlantUML

```{#fig:uml .plantuml caption="クラス図"}
サブクラス -|> 親クラス
```

# コード

ハローを @lst:hello に示す。


```{#lst:hello .cpp caption="ハローワールド"}
#include <iostream>

int main() { //ハロー
std::cout << "hello world" << std::endl;
return 0;
}
```

# 表

@tbl:table のサンプル


| No | 項目   |
|----|--------|
| 1  | りんご |
| 2  | みかん |
: フルーツ {#tbl:table}

# 結論

おしまい

下記のようなコマンドでPDFを生成する。

docker run --rm \
       --volume "$(pwd):/data" \
       --user $(id -u):$(id -g) \
       pandoc-eisvogel \
       ./README.md \
       --filter pandoc-plantuml \
       --filter pandoc-crossref \
       --metadata-file="./crossref_config.yaml" \
       --standalone \
       --data-dir=/opt/pandoc \
       --template eisvogel \
       --pdf-engine lualatex \
       -V lang=ja \
       -V luatexjapresetoptions=haranoaji \
       -V CJKmainfont=HaranoAjiGothic \
       -V caption-justification=centering \
       -N \
       --toc \
       -M listings \
       --listings \
       -H ./header.tex \
       -V linkcolor=blue \
       -V table-use-row-colors=true \
       -V titlepage=true \
       -V toc-own-page=true \
       -V toc-title="目次" \
       -o README.pdf

出力結果

f:id:mickey_happygolucky:20220407170925p:plain

f:id:mickey_happygolucky:20220407170955p:plain

f:id:mickey_happygolucky:20220407171100p:plain

f:id:mickey_happygolucky:20220407171119p:plain

まとめ

下記の機能は実現できた。

  • PlantUMLが使用できる
  • Eisvogelを使用してきれいなPDFを出力できる
  • pandoc-crossrefが使用できる

Eisvotelを使用すると簡単で非常にきれいなPDFが作成できるので、これを使う例はネット上ではよく見つかるが、 カスタマイズは結構面倒なので自分が使いたい機能を全て使えるようにするような記事は見つからなかった。

これで自分のやりたいことができる環境が作れたと思う。

pandoc-crossrefとEisvogelを併用する

はじめに

pandoc-crossrefフィルタとEisvogelを併用すると下記のようなエラーが発生する。

Error producing PDF.
! LaTeX Error: Environment lstlisting undefined.

See the LaTeX manual or LaTeX Companion for explanation.
Type  H <return>  for immediate help.
 ...                                              
                                                  
l.403 \begin{lstlisting}

エラーレポートはよく見かけるが、そのものズバリな解決策が見つけられないので調べた。

  1. pandoc-crossref: pandoc-latex-templateの--listingsオプションと併用できない
  2. Captions for code listings produce \begin{codelisting} which is not in the listings package

Eisvogelというよりは、\usepackage{listings}を含むpandoc-latex-templateとpandoc-crossrefを併用すると、このエラーに遭遇するらしい。

P.S. You also seem to be including \usepackage{listings} in your template. If you want to use that one, pandoc-crossref supports that, but needs some coercion. See https://github.com/lierdakil/pandoc-crossref#customization (in particular, listings metadata attribute)

このような記載があるが、なにをどうしたらよいのかよく分からん。

解決策

実はこのリポジトリのdemoにしれっと回避策が提供されている。

docs/demo/Makefileの例のように-M listingsコマンドラインに追加すると良い。

例えばこんな感じ。

pandoc test.md \
       --filter pandoc-crossref \
       --template eisvogel \
       --pdf-engine lualatex \
       -M listings \
       --listings \
       -o test.pdf

結論

Eisvogelに限らず、pandoc-crossrefと\usepackage{listings}を含むpandoc-latex-templateとpandoc-crossrefを併用すると、エラーが発生する。

-M listingsコマンドラインに追加すると回避できる。