Skip to content

内部実装

コミット 0d888c2 のソースを読んだもの。ここでの主張はすべてファイルと行を指す。

コードマップ

パス責務
cmd/frontend/BuildKit が実行するフロントエンドバイナリ。エントリポイントと内部サブコマンド
リポジトリルート (package dalec)spec のデータモデルと共通ロジック: spec.goload.gosource*.goartifacts.gotests.go、generator 群
frontend/ルーター、リクエスト処理、ターゲットフォワーディング、署名 (MaybeSign)
targets/linux/rpm/distro/RPM distro ハンドラ (Azure Linux・AlmaLinux・Rocky Linux)
targets/linux/deb/distro/DEB distro ハンドラ (Debian・Ubuntu)
targets/windows, targets/pluginWindows ターゲットと外部フロントエンドのフォワーディング
packaging/linux/rpm, packaging/linux/debspec → .spec / spec → debian/ の変換と rpmbuild/dpkg の LLB

中核データ構造

Spec (spec.go:20) はパース済み YAML のルートである。NameVersionRevision (spec.go:22, spec.go:29, spec.go:32)、License (spec.go:75)、Sources map[string]Source (spec.go:56)、Patches map[string][]PatchSpec (spec.go:63)、Build ArtifactBuild (spec.go:66)、Artifacts (spec.go:82)、Targets map[string]Target (spec.go:85)、Dependencies *PackageDependencies (spec.go:89)、Image *ImageConfig (spec.go:95)、Tests []*TestSpec (spec.go:103) を持つ。各 distro ハンドラはこの 1 つの構造体から読む。

Source (source.go:31) はソース種別のタグ付き union だ。DockerImageGitHTTPContextBuildInlineLLB があり、不変条件はコメントに明記されている。厳密に 1 つだけが非 nil でなければならない (source.go:33)。フィルタ用フィールド (PathIncludesExcludes) は source.go:46, source.go:50, source.go:52 にあり、Generate []*SourceGenerator (source.go:65) が gomod/cargohome/pip のキャッシュ generator を駆動する。カスタム UnmarshalYAML (source.go:70) が元の YAML ノードを source map として保持する。

RouterRoute (frontend/router.go:73, frontend/router.go:49) がディスパッチの中核だ。RouteFullPath (frontend/router.go:51)、gwclient.BuildFunc 型の Handler (frontend/router.go:54)、Info Target (frontend/router.go:57)、任意の Forward *Forward (frontend/router.go:61) を持つ。

distro.Config (targets/linux/rpm/distro/distro.go:14) は distro 1 個分の設定だ。FullNameImageRef (distro.go:15, distro.go:16)、ReleaseVer (distro.go:20)、BuilderPackages (distro.go:23)、BasePackages []dalec.Spec (distro.go:26)、InstallFunc PackageInstaller (distro.go:31)、CacheName/CacheDir (distro.go:35, distro.go:41) を持つ。Azure Linux・AlmaLinux・Rocky Linux はそれぞれこの構造体の値であり、これが RPM コードパスの共有を成り立たせている。

RPM の .spec ファイルは specTmpl (packaging/linux/rpm/template.go:23)、すなわち text/template から生成される。NameVersionReleaseLicenseSummary (template.go:25) を埋め、続いて %descriptionPrepareSourcesBuildStepsInstallPostFilesChangelog の各節を生成する (template.go:44)。このテンプレートが、Dalec spec が RPM spec ファイルへ変わる具体的な地点だ。

追う価値のあるパス

azlinux3/container ターゲットを端から端まで追う。spec → RPM → 最小コンテナである。

text
HandleContainer(cfg)          targets/linux/rpm/distro/distro.go:125
  -> Config.BuildContainer     container.go:17
       -> Config.BuildPkg      pkg.go:47
            spec.Preprocess    pkg.go:54   ソース generator を実行
            rpm.BuildRoot      pkg.go:58   rpmbuild ツリーを組む
            rpm.Build          pkg.go:70   rpmbuild を LLB として実行
       -> frontend.MaybeSign   pkg.go:72   要求があれば署名
       -> cfg.Install(pkgs)    container.go:68  RPM を /tmp/rootfs へインストール

ルートのハンドラは linux.HandleContainer(cfg) (targets/linux/rpm/distro/distro.go:125) で、Config.BuildContainer を呼ぶ (targets/linux/rpm/distro/container.go:17)。コンテナのビルドにはまずパッケージが要るので、Config.BuildPkg (targets/linux/rpm/distro/pkg.go:47) がビルド依存入りの worker イメージを用意し (pkg.go:50)、spec.Preprocess で spec の generator を実行し (pkg.go:54)、rpm.BuildRootrpmbuild ツリーを組み (pkg.go:58)、.spec のパスを SPECS/<name>/<name>.spec に確定し (pkg.go:60)、rpm.Buildrpmbuild を LLB として実行する (pkg.go:70)。

署名は結果に上書きする。frontend.MaybeSign (pkg.go:72) が署名済み state を作り、st.File(llb.Copy(signed, "/", "/")) で未署名の出力へコピーする (pkg.go:76)。未署名ビルドと署名ビルドは、この上書きステップだけが違う。

BuildContainer は続いてパッケージを新しい root ファイルシステムへインストールする。spec.GetSingleBase(targetKey) でベースを選び (container.go:23)、インストール時リポジトリをマウントし (container.go:31)、worker 上で cfg.Install(pkgs, ...) を実行して RPM を /tmp/rootfs に置き (container.go:68)、spec が post-install symlink を宣言していれば InstallPostSymlinks を適用し (container.go:76)、rootfs state を返す。これらのステップはすべて LLB グラフへの追記であり、BuildKit がそれを並列・キャッシュ付きで solve する。

読んで驚いた点

フロントエンドはビルドを一切実行しない。上記の各関数は、完成した成果物ではなく LLB state を返す。HandleContainer のチェーン全体がグラフの構築で、実際の rpmbuild・インストール・コピーは BuildKit がグラフを solve するときにだけ起きる。この反転こそが、Dalec がビルドサーバを要さず、キャッシュと並列性を BuildKit から無償で得られる理由だ。

プラグイン拡張はレジストリではなくルート上書きである。Router.Add は、同じ FullPath を持つ後のルートが前のルートを黙って置き換えることを許し、コメントはこれが意図的だと述べる。ターゲットフォワーディングがこれを使ってビルトインを上書きする (frontend/router.go:79)。spec が外部フロントエンドを指すとき、その上書きを仕込むのが WithTargetForwardingHandler (frontend/router.go:399) だ。Dalec の拡張とは、別建てのプラグインテーブルに登録するのではなく、ビルトインのルートを影で覆うことを意味する。

ルーターはすべてのリクエストを recover で包む。Router.Handler は、あらゆる panic を join されたエラー返却に変える recover を defer する (frontend/router.go:91)。そのため、あるターゲットのハンドラのバグは、BuildKit が対話しているフロントエンドプロセスを落とすのではなく、ビルドエラーとして表面化する。