Goブログ

Goにおけるパッケージバージョン管理の提案

Russ Cox
2018年3月26日

はじめに

8年前、Goチームはgoinstallgo getにつながった)と、今日のGo開発者にとってなじみのある分散型でURLのようなインポートパスを導入しました。goinstallをリリースした後、最初に人々が尋ねた質問の1つは、バージョン情報をどのように組み込むかということでした。私たちは知らないと認めました。長い間、パッケージバージョン管理の問題はアドオンツールで最適に解決されると信じており、人々が作成することを奨励しました。Goコミュニティは、さまざまなアプローチを持つ多くのツールを作成しました。それぞれが私たち全員が問題をよりよく理解するのに役立ちましたが、2016年半ばまでに、ソリューションが多すぎることは明らかでした。私たちは単一の公式ツールを採用する必要がありました。

2016年7月のGopherConで始まり秋まで続いたコミュニティディスカッションの後、私たちは皆、RustのCargoが例示するパッケージバージョン管理アプローチに従うことで答えが得られると信じていました。すなわち、タグ付けされたセマンティックバージョン、マニフェスト、ロックファイル、そして使用するバージョンを決定するためのSATソルバーです。Sam Boyerは、このおおよその計画に従い、goコマンド統合のモデルとして機能させることを意図したDepを作成するチームを率いました。しかし、Cargo/Depアプローチの意味について詳しく学ぶにつれて、特に後方互換性に関して、Goがいくつかの詳細を変更することによって利益を得ることは私にとって明らかになりました。

互換性の影響

Go 1の最も重要な新機能は、言語機能ではありませんでした。それは、Go 1の後方互換性の重視でした。それまで、私たちは約1ヶ月ごとに、互換性のない重大な変更を伴う安定版リリーススナップショットを発行していました。Go 1のリリース直後から、関心と採用が大幅に加速したことを観察しました。私たちは、互換性の約束により、開発者がGoを本番環境で使用することを非常に快適に感じ、それが今日のGoの人気が高まっている主な理由であると考えています。2013年以来、Go FAQでは、パッケージ開発者が同様の互換性の期待をユーザーに提供することを奨励しています。これをインポート互換性ルールと呼びます。「古いパッケージと新しいパッケージのインポートパスが同じである場合、新しいパッケージは古いパッケージと後方互換性がある必要があります。」

個別に、セマンティックバージョン管理は、Goコミュニティを含む多くの言語コミュニティでソフトウェアバージョンを記述するための事実上の標準になっています。セマンティックバージョン管理を使用すると、後のバージョンは以前のバージョンと後方互換性があることが期待されますが、単一のメジャーバージョン内でのみです。v1.2.3はv1.2.1およびv1.1.5と互換性がある必要がありますが、v2.3.4はそれらのいずれとも互換性がある必要はありません。

ほとんどのGo開発者が期待するように、Goパッケージにセマンティックバージョン管理を採用する場合、インポート互換性ルールでは、異なるメジャーバージョンは異なるインポートパスを使用する必要があります。この観察により、v2.0.0以降のバージョンで、メジャーバージョンをインポートパスに含めるセマンティックインポートバージョン管理に至りました:my/thing/v2/sub/pkg

1年前、私はインポートパスにバージョン番号を含めるかどうかは主に好みの問題であり、それらを持つことが特にエレガントであるとは懐疑的でした。しかし、その決定は好みの問題ではなく、論理の問題であることが判明しました。インポート互換性とセマンティックバージョン管理は、一緒にセマンティックインポートバージョン管理を必要とします。私がこれを理解したとき、論理的な必然性に驚きました。

また、セマンティックインポートバージョン管理への2番目の独立した論理ルートがあることにも驚きました。段階的なコード修復または部分的なコードアップグレードです。大規模なプログラムでは、プログラム内のすべてのパッケージが、特定の依存関係のv1からv2に同時に更新されることを期待するのは非現実的です。代わりに、プログラムの一部がv2にアップグレードされている間、プログラムの一部でv1を引き続き使用できる必要があります。しかし、プログラムのビルドとプログラムの最終バイナリには、依存関係のv1とv2の両方が含まれている必要があります。それらに同じインポートパスを与えると、混乱につながり、インポート一意性ルールと呼ぶことができるものに違反します。異なるパッケージは異なるインポートパスを持つ必要があります。部分的なコードアップグレード、インポートの一意性、およびセマンティックバージョン管理を行う唯一の方法は、セマンティックインポートバージョン管理も採用することです。

もちろん、セマンティックインポートバージョン管理なしでセマンティックバージョン管理を使用するシステムを構築することは可能ですが、部分的なコードアップグレードまたはインポートの一意性を諦めることによってのみ可能です。Cargoは、インポートの一意性を放棄することにより、部分的なコードアップグレードを可能にします。特定のインポートパスは、大規模なビルドのさまざまな部分で異なる意味を持つ可能性があります。Depは、大規模なビルドに関与するすべてのパッケージが、特定の依存関係について単一の合意されたバージョンを見つける必要があるため、部分的なコードアップグレードを諦めることにより、インポートの一意性を保証し、大規模なプログラムがビルド不能になる可能性を高めます。Cargoは、大規模なソフトウェア開発に不可欠である部分的なコードアップグレードを主張するのが正しいです。Depは、インポートの一意性を主張するのも同様に正しいです。Goの現在のベンダーサポートの複雑な使用は、インポートの一意性に違反する可能性があります。それらが違反した場合、結果として生じる問題は、開発者とツールの両方にとって理解するのが非常に困難でした。部分的なコードアップグレードとインポートの一意性のどちらをあきらめる方がより大きな痛みを伴うかを予測する必要があります。セマンティックインポートバージョン管理を使用すると、選択を回避し、両方を維持できます。

また、インポート互換性がバージョン選択をどれだけ簡略化しているかを発見したことにも驚きました。バージョン選択は、特定のビルドに使用するパッケージバージョンを決定する問題です。CargoとDepの制約により、バージョン選択はブール充足可能性の解決と同等になり、有効なバージョン構成が存在するかどうかを判断するのが非常にコストがかかる可能性があります。さらに、有効な構成は多数存在する可能性があり、「最適」な構成を選択するための明確な基準はありません。代わりにインポート互換性に依存すると、Goは常に存在する単一の最適な構成を見つけるために、些細な線形時間アルゴリズムを使用できます。私が最小バージョン選択と呼ぶこのアルゴリズムは、個別のロックファイルとマニフェストファイルの必要性を排除します。それらを、開発者とツールの両方が直接編集する単一の短い構成ファイルに置き換え、再現可能なビルドを依然としてサポートします。

Depでの私たちの経験は、互換性の影響を示しています。Cargoや以前のシステムの先例に従い、セマンティックバージョン管理を採用する一環として、インポート互換性を諦めるようにDepを設計しました。私たちはこれを意図的に決定したとは思いません。私たちは他のシステムに従っただけです。Depを使用した直接の経験は、互換性のないインポートパスを許可することによってどれだけの複雑さが生まれるかを正確に理解するのに役立ちました。セマンティックインポートバージョン管理を導入してインポート互換性ルールを復活させると、その複雑さが解消され、より単純なシステムにつながります。

進捗、プロトタイプ、提案

Depは2017年1月にリリースされました。その基本モデル(セマンティックバージョンでタグ付けされたコードと、依存関係の要件を指定する構成ファイル)は、ほとんどのGoベンダーリングツールからの明確な前進であり、Dep自体に収束することもまた、明確な前進でした。私は、特に開発者が自分たちのコードと依存関係の両方について、Goパッケージバージョンについて考えることに慣れるのに役立つように、その採用を心から奨励しました。Depは明らかに私たちを正しい方向に進めていましたが、私は詳細に潜む複雑さの悪魔について懸念していました。特に、大規模なプログラムでの段階的なコードアップグレードに対するDepのサポートがないことを懸念していました。2017年の過程で、私はSam Boyerやパッケージ管理ワーキンググループの残りのメンバーを含む多くの人と話しましたが、複雑さを軽減する明確な方法を誰も見つけることができませんでした。(私はそれに加える多くのアプローチを見つけました。)年末に近づいても、SATソルバーと充足不能なビルドが私たちができる最善の方法であるように思えました。

11月中旬、Depが段階的なコードアップグレードをどのようにサポートできるかをもう一度検討しようとしていたところ、インポート互換性に関する以前のアドバイスがセマンティックインポートバージョン管理を意味することに気づきました。それは本当のブレークスルーのように思えました。私は、後のセマンティックインポートバージョン管理のブログ投稿の最初のドラフトを書き、Depがこの規約を採用することを提案することで締めくくりました。私は草案を話し合っていた人々に送り、それは非常に強い反応を引き出しました。誰もがそれを愛しているか、嫌っていました。私は、アイデアをさらに広める前に、セマンティックインポートバージョン管理の意味をもっと解明する必要があることに気づき、それに取り掛かりました。

12月中旬、インポート互換性とセマンティックインポートバージョン管理が組み合わさると、バージョン選択を最小バージョン選択にまで削減できることを発見しました。私はそれを理解するために基本的な実装を書き、それがなぜそれほど単純なのかの背後にある理論を学ぶのに時間を費やし、それを説明する投稿のドラフトを書きました。それでも、Depのような実際のツールでこのアプローチが実用的かどうかは確信できませんでした。プロトタイプが必要であることは明らかでした。

1月、私はセマンティックインポートバージョニングと最小バージョン選択を実装したシンプルなgoコマンドラッパーの作業を開始しました。簡単なテストはうまくいきました。月末に近づくにつれて、私のシンプルなラッパーは、多くのバージョン付きパッケージを利用する実際のプログラムであるDepをビルドできるようになりました。ラッパーにはまだコマンドラインインターフェースはありませんでした。Depをビルドしているという事実は、いくつかの文字列定数にハードコードされていましたが、アプローチが実行可能であることは明らかでした。

2月の最初の3週間は、ラッパーを完全なバージョン付きgoコマンドvgoに変え、vgoを紹介するブログ記事シリーズの草稿を作成し、それらをパッケージ管理ワーキンググループとGoチームと議論しました。そして、2月の最後の週は、ついにvgoとそれらの背後にあるアイデアをGoコミュニティ全体と共有しました。

インポート互換性、セマンティックインポートバージョニング、および最小バージョン選択のコアアイデアに加えて、vgoプロトタイプは、goinstallgo getでの8年間の経験に基づいて、小さくても重要な変更をいくつか導入しています。ユニットとしてバージョン管理されるパッケージのコレクションであるGoモジュールの新しい概念、検証可能で検証済みのビルド、およびgoコマンド全体でのバージョン認識です。これにより、$GOPATH外での作業が可能になり、(ほとんどの)vendorディレクトリが不要になります。

これらの結果として、先週提出した公式のGo提案につながりました。完全な実装のように見えるかもしれませんが、これはまだプロトタイプであり、完成させるためには全員で協力する必要があります。 golang.org/x/vgoからvgoプロトタイプをダウンロードして試すことができます。また、Versioned Goツアーを読むと、vgoの使用感がわかります。

今後の展望

私が先週提出した提案は、まさにそれです。最初の提案です。 Go開発者は私たちが知らない多くの賢い方法でGoを使用しているため、Goチームと私には見えない問題があることを知っています。提案フィードバックプロセスの目標は、現在の提案の問題を特定して対処するために全員で協力し、将来のGoリリースで出荷される最終的な実装ができるだけ多くの開発者にとってうまく機能するようにすることです。 提案に関する議論のIssueで問題点を指摘してください。フィードバックが届き次第、議論の概要FAQを更新します。

この提案を成功させるためには、Goエコシステム全体、特に今日の主要なGoプロジェクトが、インポート互換性ルールとセマンティックインポートバージョニングを採用する必要があります。それが円滑に進むように、新しいバージョニング提案をコードベースに組み込む方法について質問があるプロジェクトや、経験についてフィードバックがあるプロジェクトとの間で、ビデオ会議によるユーザーフィードバックセッションも実施します。そのようなセッションへの参加にご興味がある場合は、Steve Francia(spf@golang.org)までメールでご連絡ください。

私たちは(ついに!)Goコミュニティに、パッケージバージョニングをgo getに組み込む方法に関する単一の公式な答えを提供することを楽しみにしています。ここまで到達するのを手伝ってくれたすべての人々、そしてこれから先も手伝ってくれるすべての人々に感謝します。皆様のご協力により、Go開発者が愛してくれるようなものを出荷できることを願っています。

次の記事: Goの新しいブランド
前の記事: Go 2017アンケート結果
ブログインデックス