Skip to content

SATySFi v0.1.z のライブラリルートでのファイル配置の設計案

Takashi Suwa edited this page Dec 7, 2022 · 13 revisions

概要

SATySFiの於けるパッケージとレジストリの概念について要件を定め,ファイルの配置方法・記述方法の設計を与えます.

用語の定義

  • パッケージpackage):
    • 実装のリリースに関する単位.
  • レジストリ (registry):
    • パッケージに関するメタデータを集積する場所.
      • このメタデータは,パッケージの各バージョンの実装を取得する方法や,その実装が依存する他のパッケージのバージョン制約などを含む.
    • レジストリはメタデータを記載した何らかのファイル(またはファイル群)を何らかのURLでネットワークに向けて公開しており,ユーザはGitやHTTPSなどのプロトコルによってその内容が適宜取得できる.
    • 基本的には,有志が自身の作成したパッケージを登録できる仕組みがレジストリに備わっている.
      • レジストリの管理者などから何らかのレビューを受けて許可されねばパッケージのメタデータを登録できないものもあれば,勝手に登録できるものもある.
      • レビューが必要な場合もそうでない場合も,パッケージ名は(SNSでのユーザ識別名のように)早い者勝ちで取得する.
    • レジストリ自体も,誰でも新設できる仕組みである場合もあれば,その存在が中央集権で管理されていて許可されねば設立できないものも考えられる.

比較: 他言語の既存パッケージマネージャによるレジストリの扱い方

  • OPAM:
    • ここでいうレジストリのことは “repository” と呼んでいる.
    • 😄 レジストリは誰でも簡単に新設できる.
    • どのレジストリを使うかはユーザ側がレジストリ名を指定して $(\mathit{registryName} \mapsto \mathit{registryUrl})$ の写像として手元に保持しておく.
      • 😄 ユーザが指定したレジストリ名 $\mathit{registryName}$ はそのマシン全体で使われるが,単に表示やCLIでの指定をわかりやすくするための識別に使うものなので,コンフィグファイルの移植性などには影響しない.
      • 😰 各パッケージがどのレジストリ起源なのかの情報はコンフィグ(.opam ファイル)に現れない.
    • ユーザの手元の環境に於いて,パッケージ名は提供元レジストリによらずフラットな名前空間に並ぶ.
    • 異なるレジストリが同一パッケージ名のバージョン違いを提供している場合,それらは同一パッケージの異なるバージョンとみなされる.バージョンも一致した場合は衝突.
      • 😄 独自forkが別レジストリで同名のままリリースできて扱いやすいことがある.
      • 😰 同一のものであるという意図なくパッケージ名がレジストリ間で衝突してしまうことがある.しかもユーザにはどうしようもない.
      • 😰 バージョンの衝突も起こりうる.衝突を防ぐのがレジストリの責務なのかユーザの責務なのかも微妙なところ.
  • Cargo:
    • 😄 レジストリは誰でも新設できる.
    • どのレジストリを使うかはユーザ側がレジストリ名を指定して $(\mathit{registryName} \mapsto \mathit{registryUrl})$ の写像として手元に保持しておく.
      • 具体的には,プロジェクトの .cargo/config.tomlregistries に記載しておく.
      • OPAMと違い,レジストリ名は Cargo.toml 中で依存関係の指定に使われるなど,ビルド処理に関しても意味がある.
        • レジストリ名 $\mathit{registryName}$ はそれを使うパッケージが各々自由に定めており,同一システム内での共通化して保持する場合は $\mathit{registryUrl}$ を識別に用いる.
    • ユーザの手元の環境に於いて,パッケージ名は提供元レジストリごとの名前空間に並ぶ.すなわち, $(\mathit{registryName} \mapsto (\mathit{packageName} \mapsto \mathit{packageMetadata}))$ の形で保持されている.
      • したがって,同一パッケージ名の2パッケージも提供元レジストリが違えば別パッケージと扱われる.
      • ただし,1つのレジストリからしか提供されていないパッケージ名は,簡単のためレジストリ名を省略して依存関係を指定できる.
        • 😄 典型的には crates.io のみを使い,ほとんどの場合レジストリ名を書かずに済むので簡便である.
    • 😰 crates.io は誰でもレビューなどを経ずに簡単にパッケージが登録できる上に実質的にフラットな名前空間で扱えるので,(特に有名パッケージに似た名前の)悪意のあるパッケージの登録に脆弱.
  • Go言語:
    • ※Go言語の用語では,正確にはここでいうパッケージは「モジュール」.それより小さい,ややモジュールに近い機構がGo言語では「パッケージ」と呼ばれる.
      • Go 1.11から導入されたmodule-aware modeでは,複数の「パッケージ」をまとめた「モジュール」を認識するようになった.
    • レジストリ相当のものは「パッケージ」の名前の一部に組み込まれている
      • 各「パッケージ」はGitリポジトリやその下に掘られた1つのディレクトリで扱われている.
      • 「パッケージ」の名前は github.com/john-smith/foo/bargopkg.in/yaml.v3 という具合でほぼURLそのものであり,レジストリ相当の接頭辞が含まれている.こうしたURL然とした長い名前をソースコード中でも逐一 require に書く.
        • 😄 何らかの名前を介さず実質的にURLで指定するため,どのレジストリがパッケージを命名しているのかなどを気にする必要がない.また,実装がどこから取得されているのかも明瞭.
        • 😰 バージョン制約も(コンフィグファイルなどではなく)ソースコード中の require に書く仕組みであり,しかも複数のソースファイルが共通のパッケージを使う場合に複数回バージョン制約を書かねばならないので冗長.
        • 標準ライブラリとして提供されている一部の「パッケージ」はレジストリ名の接頭辞などを伴わず fmt などと裸の名前になっている.
  • Elm:
    • レジストリ相当の機構は存在せず,全て package.elm-lang.org で管理されている.
      • パッケージ名は必ず 〈UserName〉/〈PackageName〉 で,標準ライブラリも elm/http などとなっている.
      • 😄 ユーザごとにパッケージ名の名前空間が分かれている上に中央集権なので,悪意のある似た名前のライブラリを提供しにくい.
      • 😰 中央集権であり,勝手に独自のレジストリを立てることはできない.
      • 😰 特定の組織内で閉じたコードベースをレジストリの仕組みに乗せて扱うことなどはできない.

有志の方のご意見

要件

上記の諸々も勘案して,ひとまず以下を要件としようと考えています:

  • レジストリは複数使えて,パッケージごとにどのレジストリを使うか設定できるようにする.
    • ひとつはデフォルトレジストリと呼ぶ公式的なレジストリとし,デフォルトレジストリに登録されるパッケージはデフォルトレジストリに登録されているパッケージにしか依存できないとする.
    • 用途上,特定の組織だけに閉じた非公開のパッケージを扱うユースケースが十分考えられるため.
  • レジストリ自体を新設することが誰でも(何らかの中央集権的な仕組みに依存せず)でき,かつ限られたネットワークにしか公開しないことも可能であるようにする.
  • SATySFi自身のバージョンについては,処理系が “自身に処理できるパッケージであるか” を判定するのみとし,旧来のバージョンの挙動へのフォールバックなどはしないとする.
    • 処理できないと判定された場合はそこで異常系として終える.
    • パッケージの制約解消時に処理系が「自身のバージョンがサポートされているか」を加味する機能をつけられれればさらに望ましい.
    • 処理系本体のバージョン切り替えは “satysfiup” のような別ツールの責務とする.Satyrographosに備えつけてもよいかもしれない.
  • 取得済みのレジストリ情報やパッケージのソースは,ライブラリルートに保持する方法をデフォルトとし,かつユーザが望めばローカルにも配置できるようにする.
  • 1つのレジストリは名前空間で階層化されておらず,登録されているパッケージ名が一様に並ぶものとする.
  • 異なるレジストリが提供する同名のパッケージは,たまたま同名の別パッケージとして扱うようにする.

新しいライブラリルートの設計案

要件に基づき,ライブラリルートは以下のような構成に変更してはどうかと考えています(各々の解説は後述):

$LIBROOT/
├── satysfi-library-root.yaml
├── cache/
│   └── locks/
│       ├── foo.0.1.0.tar.gz
│       ├── foo.0.1.1.tar.gz
│       ...
├── registries/
│   ├── 165eecff8485b2d9126196c235804c68/  # github.com/SATySFi/default-registry のローカルID
│   │   └── satysfi-registry.yaml
│   ├── 1db09faa6db8ce3e6e317e721d09cdf5/  # your-gitab-enterprise.co.jp/project/registry のローカルID
│   │   └── satysfi-registry.yaml
│   ...
└── packages/
    ├── 165eecff8485b2d9126196c235804c68/
    │   ├── foo/
    │   │   ├── foo.0.1.0/
    │   │   │   ├── satysfi.yaml
    │   │   │   ├── src/
    │   │   │   ...
    │   │   ├── foo.0.1.1/
    │   │   ...
    │   ├── bar/
    │   │   ├── bar.0.0.1/
    │   │   ...
    │   ...
    ├── 1db09faa6db8ce3e6e317e721d09cdf5/
    │   ├── qux/ 
    │   │   ├── qux.1.0.0/
    │   │   ...
    │   ...
    ...
  • ./satysfi-library-root.yaml
    • ライブラリルート全体に対するメタデータを記録したファイル.
    • 内容の形式は後述.
    • SATySFiは,ライブラリルートとなりうるディレクトリの候補リストから,このファイルが存在する最初のディレクトリをライブラリルートとみなす.したがって,このファイルが削除されると「ここがSATySFiのライブラリルートである」ということをSATySFiが知る方法が失われる.
  • ./cache/
    • ネットワークを介してフェッチしたデータなど,SATySFiが動作していない任意のタイミングで削除しても振舞いに変化をきたさないファイルが配置されるディレクトリ.
    • ./cache/locks/
      • パッケージの各バージョンの実装であるtarballを置いておく場所.
  • ./registries/
    • レジストリに関する情報を置いておくディレクトリ.
      • SATySFiに於けるレジストリはOPAMに於けるregistryの概念と同じ.
    • このディレクトリ以下のファイルを削除すると,どんなレジストリを使用していたかの情報が失われ,レジストリ情報の再設定と再取得が必要になる.
    • ./registries/〈LocalRegistryId〉/
      • 1つのレジストリが提供するメタデータが置かれる場所.
      • 〈LocalRegistryId〉 はローカルマシン中でレジストリを一意に識別するためのID.
      • satysfi-registry.yaml がそのレジストリに登録されている各パッケージのインストールに必要なメタデータを記録したファイル.典型的にはGitリポジトリの特定のbranchをpullするかHTTPSでフェッチするかによりこのファイルが更新される.
  • ./packages/
    • 取得したパッケージの実装が展開・配置されるディレクトリ.
    • このディレクトリ以下のファイルを削除しても失われる情報はないが,該当ファイルに依存する既存の文書が正常に処理できなくなる.

satysfi-library-root.yaml の記述形式

LibraryRootConfig := {
  "language": String,
  //想定するSATySFiのバージョン

  "registries": Array[Registry],
  //マシン内で使われていて取得済みのレジストリのリスト
}

Registry := {
  "url": String,
  //レジストリURL

  "local_id": String,
  //ローカルマシン中でレジストリを一意に識別するためのID.
  //正規化したURLのmd5sumなど,URLから函数的に得られるものが望ましい.
  //主としてレジストリの内容をファイルシステムにマップするために使う.

  "fetch_protocol": RegistryProtocol,
  //satysfi-registry.yaml を更新する際の取得プロトコルの指定
}

RegistryProtocol := "git" | "http"

satysfi-registry.yaml の記述形式

各レジストリが提供するファイル.

RegistryConfig := {
  "language": String,
  //期待するSATySFiのバージョン

  "allow_external_registry": Boolean,
  //このレジストリに登録されているパッケージが,
  //他のレジストリに登録されているパッケージに依存してよいか否か.
  //デフォルトでは true

  "packages": Array[PackageSpec],
  //登録されているパッケージたち
}

PackageSpec := {
  "name": String,                     //パッケージ名
  "implementations": Array[ImplSpec], //提供されているバージョン一覧
}

ImplSpec := {
  "version": String,                 //バージョン
  "source": SourceSpec,              //ソースコードの取得方法
  "dependencies": Array[Dependency], //依存パッケージ
}

SourceSpec := TarGzipSourceSpec | ...

TarGzipSourceSpec := {
  "type": "tar_gzip",
  "url": String, //tarballを取得するためのURL
}

Dependency := {
  "name": String,                 //依存パッケージ名
  "registry": DependencyRegistry, //提供元レジストリ.省略時は { "type": "self" }
}

DependencyRegistry :=
  | { "type": "self" }
  | { "type": "external", "url": String }
//依存パッケージの提供元レジストリの指定.
//{ "type": "self" } は自身のレジストリを指し,
//{ "type": "external", ... } は他のレジストリを指す.
//"allow_external_registry" が false の場合は { "type": "self" } のみ許容.

参考文献

  1. Registries - The Cargo Book
  2. Configuration - The Cargo Book
  3. New module changes in Go 1.16 - The Go Programming Language
  4. Go のモジュール管理【バージョン 1.17 改訂版】 - Spiegel | Zenn
Clone this wiki locally