ビルド済みパッケージ管理 調査ノート
Nix、aqua、Homebrew、Spack、Conda/Pixi、pkgx、Python wheelなどは、いずれも「利用者がsource buildせずにsoftwareを使える」体験を提供し得る。しかし内部では、何をpackage identityとするか、誰がbuildするか、artifactがない場合どうするか、どこまで互換性をmetadata化するか、誰をtrustするかが大きく異なる。
このMOCでは、それらを製品別ではなく設計論点ごとに分解する。
まず配布モデルを分ける
入口は ビルド済みパッケージ配布を一括りにしない。
大きく見ると、次の系統がある。
- Nix / Guix: build resultをsubstituteする Nixのsubstituteは「バイナリパッケージ」よりbuild resultに近い
- Homebrew / Spack: recipe/specとbinary cacheを併用する
- aqua: upstream release assetをregistryで選ぶ aquaはupstream releaseを正規化するパッケージマネージャーと捉えられる
- Conda / Pixi: dependency metadataを持つbinary package群をsolverで解く
- apt/rpm: distributionが作ったbinary repositoryを一次的なpackage universeとする
- mise: 複数の異質な方式を同じcontrol planeの下に置く miseは異なる配布モデルをbackendで同じUXに重ねる
cacheとrepositoryの差は binary cacheとpackage repositoryは似ているが一次性が違う で扱う。両者は同じobject storage/CDNを使えても、cache miss時の意味が違う。
build resultをどう識別するか
Nixのstore pathにhashがあるからといって、通常それがartifact bytesのcontent hashというわけではない。Nixのstore pathは通常content-addressedではない で、build input由来のidentityとcontent由来のidentityを分ける。
content-addressingにも限界がある。content addressingはprovenanceではない の通り、digestは「何bytesか」は表せても、「どのsourceをどのbuilderでbuildしたか」は表さない。
この構造はpackage manager固有ではない。Bazel Remote Cacheもaction hashからresultを引き、actual fileはCASへ置く。Bazel Remote Cacheはpackage binary cacheと同じ問題をaction粒度で扱う では、package binary cacheとremote build cacheを同じprimitiveの延長として比較する。
再現性を一語で扱わない
lockfileで依存を固定してもreproducible buildにはならない では、
- dependency resolutionが同じ
- build environmentが同じ
- output bytesがbit-for-bit同じ
を分離する。
lockfileやderivationでinputsを固定しても、timestamp、randomness、filesystem order、absolute build path等がoutputへ入ればreproducible buildにはならない。
prebuiltがないときのsource build fallbackは同じ意味ではない では、prebuiltが無かった場合のfallbackも同じ意味ではないことを整理する。Nixではsubstitute missから本来のderivation buildへ戻るが、aquaではupstream binaryがないplatformに別のGo build methodを定義する形であり、prebuiltとlocal buildが同一bytesであることを前提にはしない。
binary portabilityを分解する
「別machineで動くか」には少なくとも二つの問題がある。
install location
ビルド済みバイナリのrelocationには複数の解法がある では、
- Homebrew: relocatable Bottleか判定し、無理なら使わない
- Spack: embedded prefixを書き換える
- Conda: placeholder/
has_prefixをpackage formatに持つ - Nix:固定store namespaceを前提にして任意prefix relocationを避ける
という戦略差を扱う。
pkgxはrelocatableなversioned POSIX prefixを配る はさらに別の例で、package producer側に最初からrelocatableなPOSIX prefixを要求する。
runtime ABI
linux/amd64だけではビルド済みバイナリの互換性を表せない では、linux/amd64だけではglibc、CUDA、CPU feature等を表せない問題を扱う。
Condaは__glibcや__cuda等をvirtual packageとしてsolver入力にする。一方 Python wheelはinterpreter・ABI・platformをartifact名に埋め込む では、Python wheelがinterpreter・ABI・platform compatibilityをartifact filename/tagへencodeする方法を見る。manylinux/musllinuxはLinuxのABI baselineをportable distribution contractとして表現する例になる。
互換性を細かくするとartifact数が増える。prebuilt binaryはbuild matrixが増えるほどcache効率が落ちる では、OS × architecture × libc × compiler × runtime × variantの組み合わせと、cache storage/index/hit rateのtrade-offを扱う。
upstream binary型ではregistryが重要になる
aqua Registryはupstream releaseの不統一を正規化する では、aqua Registryがprojectごとに異なるrelease asset名、archive形式、OS/arch表記、version途中の命名変更をdeclarative metadataで吸収していることを見る。
これは単なるpackage catalogではなく、upstream release schemaのcompatibility databaseと捉えられる。
そのためregistry自体がtrust boundaryになる。package registry metadata自体がsupply-chain trust boundaryになる では、metadataがdownload URL、asset selection、verification method、executable fileを決めること、またmiseのbundled aqua registry snapshotとfloating registryのtrade-offを扱う。
package definitionにどの程度の表現力を与えるかもsecurityに直結する。installer scriptを許すほどpackage managerのtrust surfaceは広がる では、任意shell installerを許すasdf plugin型と、download + unarchive中心に制限するaqua型を比較する。
integrity・authenticity・provenance・freshnessを分ける
checksum・signature・provenanceは別の保証をする がsecurityの中心ノート。
- checksum: 期待したbytesか
- signature: trusted signer/identityが認証したか
- provenance: どのsource/input/builder/processから作られたか
は別の保証である。
Nix/Guix型ではさらに binary cacheの公開鍵を追加することはbuilderを信頼することに近い が重要になる。input-addressed objectについてcache signing keyをtrustすることは、cache operatorに「このstore pathのresultはこれだ」と証言する強いauthorityを与えることに近い。
signatureが正しくても古いartifactを再提示される問題は残る。署名検証だけではrollback・freeze attackを防げない では、TUFを例にversion/expiration/Timestamp/Snapshotを使ってfreshnessとrepository consistencyを検証する理由を扱う。
distribution protocol自体はpackage semanticsから分離できる
OCI Registryはcontainer image以外のcontent distribution primitiveにもなる では、OCI Distribution Specificationをcontainer image専用ではなく、content-addressed blobとmanifestをpush/pullするcontent distribution protocolとして見る。
OCI RegistryはNix型build cache、Spack cache、一般binary repository等のbackendになり得るが、dependency solver、ABI compatibility、version semantics、build recipeは定義しない。つまりstorage/distribution protocolを標準化してもpackage semanticsは別に必要である。
この調査から見える比較軸
ビルド済みpackage systemを比較する際、最低限次を確認すると設計差が見える。
- package identityはversion、recipe/spec、dependency closure、content digestのどこにあるか
- artifactを誰がbuildするか — upstream、distribution、cache operator、local user
- prebuiltが無い場合にbuildできるか、そのbuild definitionは同じか
- relocationを固定prefix、rewrite、producer requirementのどこで解くか
- ABI compatibilityをartifact tag、solver metadata、registry rule、暗黙baselineのどこで表すか
- checksum・signature・provenanceのどこまで検証するか
- registry/cache signing keyはどのauthorityを持つか
- rollback/freezeをどう防ぐか
- build matrixとcache storageをどう制御するか
- package definitionがarbitrary code executionを許すか
- registry metadataをpinするかfloatさせるか
- storage protocolとpackage semanticsを分離できるか
「binary package manager」という一語では、この差の大部分が消えてしまう。Nixとaquaが同じ問題を別解で解いているというより、そもそも一次的に解いている問題が異なると捉える方がよい。
出典
- Nix 2.35 Manual
- GNU Guix Reference Manual
- aqua Documentation
- Homebrew: Bottles
- Spack: Build Caches
- Conda: Managing virtual packages
- PyPA: Platform compatibility tags
- pkgx Documentation
- mise: Backends
- OCI Distribution Specification
- Bazel: Remote Caching
- SLSA 1.2
- The Update Framework
- Reproducible Builds