タラバガニー設計局stalins.clubNOTE/notes/prebuilt-package-management-moc

ビルド済みパッケージ管理 調査ノート

Nix、aqua、Homebrew、Spack、Conda/Pixi、pkgx、Python wheelなどは、いずれも「利用者がsource buildせずにsoftwareを使える」体験を提供し得る。しかし内部では、何をpackage identityとするか、誰がbuildするか、artifactがない場合どうするか、どこまで互換性をmetadata化するか、誰をtrustするかが大きく異なる。

このMOCでは、それらを製品別ではなく設計論点ごとに分解する。

まず配布モデルを分ける

入口は ビルド済みパッケージ配布を一括りにしない

大きく見ると、次の系統がある。

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にはならない では、

  1. dependency resolutionが同じ
  2. build environmentが同じ
  3. 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を比較する際、最低限次を確認すると設計差が見える。

  1. package identityはversion、recipe/spec、dependency closure、content digestのどこにあるか
  2. artifactを誰がbuildするか — upstream、distribution、cache operator、local user
  3. prebuiltが無い場合にbuildできるか、そのbuild definitionは同じか
  4. relocationを固定prefix、rewrite、producer requirementのどこで解くか
  5. ABI compatibilityをartifact tag、solver metadata、registry rule、暗黙baselineのどこで表すか
  6. checksum・signature・provenanceのどこまで検証するか
  7. registry/cache signing keyはどのauthorityを持つか
  8. rollback/freezeをどう防ぐか
  9. build matrixとcache storageをどう制御するか
  10. package definitionがarbitrary code executionを許すか
  11. registry metadataをpinするかfloatさせるか
  12. storage protocolとpackage semanticsを分離できるか

「binary package manager」という一語では、この差の大部分が消えてしまう。Nixとaquaが同じ問題を別解で解いているというより、そもそも一次的に解いている問題が異なると捉える方がよい。

#moc #ツールチェーン #security

出典

▸ ノート一覧に戻る