継続的インテグレーション

継続的インテグレーション (continuous integration, CI) システムとは、ソフトウェアのビルドとテストを定期的に自動で行うシステムのことです。CI システムの一番のメリットはビルドとテストの間にある長い時間を無くせることですが、それだけではなく面倒なタスクを単純化および自動化できるというメリットもあります。例えばクロスプラットフォームのテスト、低速なテスト、大量のデータが必要なテスト、構成が難しいテスト、レガシープラットフォームにおける正確なパフォーマンスの検証、稀にしか落ちないテストの検出、最新のリリースの定期的な生成などがそうです。さらに継続的インテグレーションを実装する上でビルドとテストの自動化が不可欠なことから、CI は継続的デプロイフレームワークに向けた最初の一歩となります。ここで継続的デプロイ (continuous deployment) とは、テストが済んだソフトウェアのアップデートを素早く本番システムにデプロイする仕組みのことです。

アジャイルというソフトウェア開発手法が継続的インテグレーションを使っていることもあり、CI はタイムリーな話題です。最近になってオープンソースの CI ツールは爆発的に増えており、様々な言語で書かれた、様々な言語向けの、膨大な機能を様々なアーキテクチャモデルの下で実装した CI ツールが存在します。この章では継続的インテグレーションシステムにおいてよく実装されている機能を説明し、利用可能なアーキテクチャの選択肢を概観し、各アーキテクチャにおいて実装が簡単になる、あるいは難しくなる機能について議論します。

これから、CI システムの設計において可能なアーキテクチャの例を分かりやすく示すシステムをいくつか簡単に説明します。最初の Buildbot はマスター/スレーブモデル、二つ目の CDash はレポートサーバーモデル、三つ目の Jenkins はハイブリッドモデル、四つ目の Pony-Build は Python ベースの非中央集権型レポートサーバーモデルです。本章後半では最後の Pony-Build を軸としてさらに議論します。

概観

継続的インテグレーションシステムのアーキテクチャは両極端にある二つのアーキテクチャによって支配されているように思えます。つまり中央サーバーがリモートビルドの指示と制御を行うマスター/スレーブアーキテクチャと、中央サーバーがクライアントからのビルドレポートの集約を行うレポートアーキテクチャです。私たちが見たことのある継続的インテグレーションシステムはどれもこの二つのアーキテクチャが組み合わさったものを選択しています。

中央集権型アーキテクチャの例は BuildBot であり、このシステムは二つの部分からなっています。一つはビルドのスケジュールと調整を行う中央サーバー、またの名を buildmaster であり、もう一つはビルドを実行するクライアント、またの名を buildslave です。buildmaster はクライアントの接続場所を提供すると共に、クライアントが実行するコマンドとその順番についての設定情報を保持します。buildslave は buildmaster に接続し、詳細な指示を受けます。buildslave の設定で行える処理にはソフトウェアのインストール、マスターサーバーの識別、マスターサーバーに接続するための接続証明書の取得などがあります。buildslave の出力は buildmaster に送られ、ウェブやその他のレポートと通知行うシステムを通じた閲覧のために保存されます。

CI システムのアーキテクチャのもう一端にあるのが CDash が採用したアーキテクチャです。CDash は Kitware, Inc. による Visualization Toolkit (VRK) プロジェクトおよび Insight Toolkit (ITK) プロジェクトで使われています。CDash は本質的にはレポートサーバーであり、CMake と CTest を実行するクライアントから受け取った情報を保存し、それを見やすく提示するように設計されています。クライアントはビルドを行い、テストスイートを実行し、ビルドとテストの結果を記録し、CDash サーバーに接続してその情報を送信します。

最後に、三つ目のシステム Jenkins (2011 年に名前が変更されるまでは Hudson として知られていました) は両方の動作モードをサポートします。Jenkins ではビルドをノードで独立に実行させてからその結果をマスターサーバー遅らせることもできますし、マスターサーバーがビルドの実行のスケジュールおよび調整を行うこともできます。

中央集権モデルと非中央集権モデルの両方に存在する機能もあり、Jenkins のように両方のモデルが単一の実装に共存することも可能です。しかし、Buildbot と CDash は正反対のシステムです。ソフトウェアをビルドしてそれを報告するという同じことを行っているにも関わらず、基本的なアーキテクチャの全てが異なっているのです。どうしてでしょうか?

疑問は尽きません。異なるアーキテクチャを選択したことで、様々な機能の実装がどの程度簡単に、あるいは難しくなるのでしょうか?中央集権モデルだからこそ自然に生まれた機能というはあるのでしょうか?既存の実装はどの程度拡張可能なのでしょうか?新しいレポートメカニズムを提供したり、多くのパッケージに対してスケールしたり、クラウド環境においてビルドとテストを実行するようシステムを簡単に変更するできるでしょうか?

CI システムは何をするのか

継続的インテグレーションシステムのコアとなる機能は単純です。ソフトウェアをビルドし、テストを実行し、その結果をレポートするだけです。ビルド、テスト、レポートはスクリプトで実行でき、そのスクリプトはスケジュールされたタスクあるいは cron のジョブとして実行できます。つまり VCS からソースコードを新しくチェックアウトし、ビルドを行い、テストを実行するという単純なスクリプトです。ビルドの出力をファイルに記録したり、あらかじめ決めておいた場所に保存したり、ビルドが失敗した場合には E メールで知らせるようにもできます。以上の処理の実装は簡単であり、例えば UNIX では Python パッケージに対してこの処理を行う CI システムを 7 行のスクリプトで実装できます:

cd /tmp && \
svn checkout http://some.project.url && \
cd project_directory && \
python setup.py build && \
python setup.py test || \
echo build failed | sendmail notification@project.domain
cd /tmp && rm -fr project_directory

図 9.1 において、白い四角が独立したサブシステムおよびその機能を表し、矢印はコンポーネント間の情報の流れを示します。雲がビルドプロセスの中でリモートで実行できる部分を表し、灰色の四角がサブシステムの間で切り離せない可能性のある部分を示します。例えばビルドのモニタリングはビルドプロセス自体の状態やシステムの状態 (CPU 使用率、I/O 使用率、メモリ使用量) の監視を含む可能性があります。

継続的インテグレーションの内部
図 9.1. 継続的インテグレーションの内部

しかし一見した単純さは当てになりません。現実世界の CI システムは通常もっとたくさんのことを行っています。リモートのビルドプロセスを初期化してその結果を受け取る以外に、継続的インテグレーションシステムがサポートできる機能は次のようなものがあります:

CI システムの高レベルな外観を 図 9.1 に示します。CI ソフトウェアはここに挙げた要素の一部を実装しています。

外部との対話

継続的インテグレーションシステムと他のシステムとの間で対話が必要になることもあります。考えられる対話内容をいくつか挙げます:

アーキテクチャ

Buildbot と CDash は正反対のアーキテクチャを選択しており、実装されている機能には重なる部分もありますが全く異なる部分もあります。以下では二つのシステムが持つ機能を説明し、それぞれの機能の実装が選択されたアーキテクチャによってどれくらい簡単になっているか、あるいはどれくらい難しくなっているのかを議論します。

実装モデル: Buildbot

Buildbot のアーキテクチャ
図 9.2. Buildbot のアーキテクチャ

Buildbot は単一の中央サーバーと複数のビルドスレーブからなるマスター/スレーブモデルのアーキテクチャを使っており、全てのリモートにおけるビルドの実行はマスターサーバーによってリアルタイムに制御されます。つまりマスターの設定が各リモートシステムで実行するコマンドを指定し、リモートはそれを順番通りに実行します。またマスターはスケジュールとビルドリクエストを調整するだけではなく、その指示も全て行います。ビルトインの指示書のための抽象化はほぼ存在せず、唯一あるのはバージョン管理システムの基本的な機能 (“コードはこのリポジトリにある”を指定する程度)、そしてビルドディレクトリに対して実行されるコマンドとディレクトリ内部で実行されるコマンドの区別だけです。OS 固有のコマンドは設定の中で直接指定されることが多いです。

buildslave で実行するジョブの管理および連携を行うために、Buildbot は全ての buildslave との間の接続を常に保持します。リモートマシンとの間で常時接続を管理するせいで実装が非常に複雑になっており、長い間バグの温床となってきました。長期間に渡ってネットワーク接続を途切れることなく保持するのは単純なことではなく、ローカルの GUI とやり取りを行うアプリケーションをネットワーク越しにテストするのは難易度が高いためです。中でも OS のアラートウィンドウが特に厄介です。しかし常時接続のおかげでマスターはジョブを実行するスレーブを完全に支配下に置けるので、リソースの調整とスケジュールは単純明快になります。

このように Buildbot モデルではスレーブを厳格に制御できるので、ビルド時に行う中央集権型のリソース調整がとても簡単です。Buildbot は buildmaster から利用可能なマスターとスレーブそれぞれに対するロックを実装しており、システム全体およびマシンごとに共有されるリソースを調整しながらビルドを行うことができます。この機能があるので、Buildbot が向いているのは統合テスト (データベースなどのアクセスに時間のかかるリソースを使うテスト) を行う大規模なソフトウェアであると言えます。

しかし中央集権型の設定は分散型のモデルで使うと問題が生じます。例えば全ての buildslave はマスターの設定を受け入れる設定を明示的にしなればならないので、新しい buildslave を動的に中央サーバーに追加してビルドを行ったり結果を送信するのは不可能です。さらに buildslave は完全に buildmaster によって制御されなければならず、クライアントは悪意ある設定や誤って生じた危険な設定に対して脆弱になります。マスターは OS のセキュリティ制限が許す限りあらゆることをクライアントに指示できます。

Buildbot の機能的な制限として、ビルドの生成物を中央サーバーに送り返させる単純な方法が無いというのがあります。例えばコードカバレッジの統計やバイナリビルドはリモートの buildslave に保存されるだけであり、それらを中央の buildmaster に集めて集計、保存するための API は存在しません。なぜこの機能が無いのかはよく分かりませんが、おそらく Buildbot が使っているコマンドがリモートでコマンドの実行させることだけに集中しているためだと思われます。あるいは buildmaster と buildslave の間の接続を RPC メカニズムではなく制御システムとして使うという決断の結果こうなったのかもしれません。

マスター/スレーブモデルおよびこの制限された通信チャンネルの結果として生じた欠点がもう一つあります。それは buildslave がシステムの利用率を報告しないために、リモートが高負荷になっていたとしても buildmaster が気づくことができない点です。

ビルド結果を外部へ CPU から通知するときには buildmaster が全ての処理を行い、通知サービスは buildmaster 自身の中で実装されています。同様に、新しいビルドリクエストを行うには buildmaster と直接通信する必要があります。

実装モデル: CDash

CDash のアーキテクチャ
図 9.3. CDash のアーキテクチャ

Buildbot とは対照的に、CDash はサーバーモデルを実装します。このモデルでは CDash が中央レポジトリとなり、リモートで実行されるビルドの情報を収集します。この情報に含まれるのは失敗したビルドやテスト、コードカバレッジの解析、メモリ使用量といった情報です。リモートクライアントはビルドを自身のスケジュールに沿って実行し、その結果を XML フォーマットで提出します。ビルドを提出できるのは“正式な”ビルドクライアントだけではなく、コア開発者でない開発者や公開されたビルドプロセスを自身のマシンで実行したユーザーも提出が可能です。

この単純なモデルが可能なのは、Kitware が開発する他のビルドインフラとの密な連携があるためです。つまりビルド構成システム CMake、テストランナー CTest、そしてパッケージシステム CPack です。これらのソフトウェアを使うと、ビルド、テスト、パッケージの各段階を OS に依存しない形で高レベルに実装できます。

CDash は処理をクライアントドリブンに行うことで、クライアント側の CI プロセスの様々な部分を単純化しています。例えばビルドを実行するという判断はクライアントによって行われるので、クライアント側の条件 (日時、負荷など) を見てからビルドを開始することが可能です。またクライアントは好きに仕事を始めたり止めたりできるので、ボランティアによるビルドや“クラウド上での”ビルドが簡単に行えます。さらにビルドの生成物を中央サーバーに送る処理は通常のアップロードメカニズムを使って簡単に行えます。

しかし、このレポートを軸としたモデルと引き換えに、CDash は Buildbot が持つ便利を機能がいくつも失っています。まずリソースを中央から調整するのは不可能であり、信用できない不安定なクライアントが参加する分散環境では実装も困難です。また進捗レポートも実装されておらず、これを行うには中央サーバーがビルド状態のインクリメンタルな更新をサポートする必要があります。さらに、これは当然なのですが、ビルドをクライアント全体にリクエストする方法はありませんし、匿名のクライアントがチェックインのたびにビルドを実行するという確証もありません。クライアントを信頼することはできないのです。

CDash は最近になって“@Home”というクラウドビルドシステムを有効化する機能を追加しました。これを使うとクライアントが CDash サーバーに対してビルドサービスを行うことができます。つまりクライアントがサーバーにビルドリクエストを poll し、リクエストがあればそれを実行し、その結果をサーバーに送り返すのです。現在 (2010 年 10 月) の実装ではビルドはサーバー側から手動でリクエストしなければならず、その時点で接続しているクライアントしかサービスを行うことができません。しかしこの機能を自然に拡張すれば、ビルドのリクエストがサーバーから利用可能なクライアントに自動的に送られるという、さらに一般的なスケジュールビルドモデルを構築することが可能なはずです。この“@Home”システムは後述する Pony-Build システムのコンセプトと非常に似ています。

実装モデル: Jenkins

Jenkins は広く使わている継続的インテグレーションシステムであり、Java で書かれています。2011 年初頭までは Hudson という名前で知られていました。ローカルのシステムで実行を行うスタンドアローンの CI システムとなることもできますし、リモートビルドを管理する CI システムにもなれます。さらにリモートで実行されたビルド情報の受け取り役となることも可能です。様々なテストツールからのレポートを統合するにあたって、Jenkins はユニットテストとコードカバレッジに関する JUnit の XML 規格を利用しています。Jenkins は Sun が起源ですが、利用者は多岐にわたり、活発なオープンソースコミュニティもあります。

Jenkins はハイブリッドモードで動作します。つまりデフォルトではマスターサーバーがビルドを行いますが、リモートビルドを行うための方法もいくつかあります。例えばサーバーの指示によるビルドとクライアントが自発的に始めるビルドは両方サポートされます。ただし本来の設計は Buildbot と同じく中央サーバーを使って制御を行うというものであり、仮想マシンの管理などの様々な分散ジョブの開始メカニズムは後から追加されたものです。

Jenkins ではマスターが複数のリモートマシンを管理することが可能であり、そのときには master からの SSH 接続またはクライアントからの JNLP (Java Web Start) が使われます。接続は両方向であり、オブジェクトやデータをシリアルトランスポートで送り合うことができます。

この接続の詳細は Jenkins の堅牢なプラグインアーキテクチャによって隠蔽されます。これによってバイナリビルドやより重要な結果データを送り返すためのサードパーティのプラグインが開発可能になっています。

また Jenkins には中央サーバーが制御するジョブが並列に実行されないようにする“locks”というプラグインが あります (2011 年 1 月 現在開発が続いています)。

実装モデル: Pony-Build

Pony-Build のアーキテクチャ
図 9.4. Pony-Build のアーキテクチャ

Pony-Build は proof-of-concept な非中央集権型 CI システムであり、Python で書かれています。Pony-Build を構成する三つの要素を 図 9.4 に示します。リザルトサーバーは中央データベースであり、クライアントから送られてくるビルド結果をまとめます。クライアントは独立して構成情報とビルドコンテキストを持ち、VCS レポジトリへのアクセスやサーバーとの通信のための軽量なクライアントライブラリを持ちます。レポートサーバーはビルド結果の報告および新しいビルドのリクエストのためのシンプルなウェブインターフェースを持ち、省略可能です。レポートサーバーとリザルトサーバーは単一のマルチスレッドプロセスとして実行されるように実装されていますが、API レベルでの結びつきは弱いので独立に実行するのは簡単です。

以上の基本的なモデルに加えて、通知やビルドイントロスぺクションを行うための webhook や RPC の仕組みがあります。例えばコードレポジトリからの VCS 通知はビルドシステムに直接結びついているのではなく、リモートのビルドリクエストはまずレポートシステムに伝えられ、その後レポートシステムがリザルトサーバーとやり取りをするようになっています。同様に E メールやインスタントメッセージなどのサービスを使うビルドのプッシュ通知はレポートサーバーと直接結びついてはおらず、そういった通知は PubSubHubbub (PuSH) というアクティブ通知プロトコルを使って制御されます。これによって様々なアプリケーションが興味あるイベントについての通知を PuSH の webhook を通して受け取ることが可能になります (ただし現在新しいビルドの完了と失敗したビルドの通知しか実装されていません)。

要素同士を切り離すこのモデルには、様々な利点があります:

残念ながら、Pony-Build のモデルには深刻な欠点もいくつかあります。どれも CDash のモデルと同様のものです:

Pony-Build によって提起された CI の側面がもう二つあります。一つはビルド指示書の実装方法、もう一つは信用の管理方法です。指示書はクライアントで任意のコードを実行できることから、この二つの問題は絡み合っています。

ビルド指示書

ビルド指示書はビルドコマンドを使いやすいように抽象化します。これはクロスプラットフォームの言語を使って複数のプラットフォームに対してビルドを行うシステムにおいて特に便利な機能です。例えば CDash は厳格な指示書を利用しています。つまり CDash を使うほとんど全てのソフトウェアは CMake、CTest、CPack を使ってビルドされ、これらのツールがマルチプラットフォームに関する問題に対処します。全ての問題を他のビルドツールチェインに任せることができるので、継続的インテグレーションシステムから見ればこれは理想的な状況と言えます。

しかし、他の言語と他のビルド環境においては状況が異なります。例えば Python のエコシステムにおいては distutils と distutils2 によるソフトウェアのビルドとパッケージの標準化が進んでいますが、テストを見つけて実行し、さらにその結果を集めるための標準はまだありません。さらに Python の複雑なパッケージは distutils の拡張メカニズムを使って特別なビルドロジックを追加するのですが、これを使うと任意のコードが実行できてしまいます。そしてこれこそが多くのビルドツールチェインで見られる問題です。つまり、実行するコマンドの標準のようなものがあるにはあるのですが、例外とか拡張などと呼ばれるものが必ず付いているのです。

そのためビルド、テスト、パッケージのための指示書は厄介な問題となります。指示書には解決すべき問題が二つあるからです。一つはプラットフォームに依存しない形でビルドコマンドを指定し、単一の指示書を使って複数のシステムでソフトウェアをビルドできるようにすること、そしてもう一つはビルドしているソフトウェアに応じてビルドをカスタマイズすることです。

信用

しかしこれによって三番目の問題が現れます。CI システムの指示書を様々な用途で使うとなると、そのシステムが使うセカンドパーティのソフトウェアを信用しなければならなりません。それもそのソフトウェアに信用が必要なだけではなく、指示書にも信用が必要になります。両方とも CI クライアントで任意のコードを実行できなければならないからです。

こういった信用の問題は統率の取れた制御が可能な環境 (例えばビルドクライアントと CI システムが内部プロセスの一部となっているような企業) では簡単に対処できます。しかしその他の開発環境においても、サードパーティが (例えばオープンソースプロジェクトに) ビルドサービスを提供する場合があります。理想的な解決法は標準的なビルド指示書をソフトウェアに含めることをコミュニティレベルで決めてしまうことであり、Python コミュニティは distutils2 でこの方法を選択しています。もう一つの解決法はデジタル署名された指示書を利用し、信用された人物だけが署名済み指示書の設定と配布を行い、CI クライアントは実行する指示書が信用できるかどうかを確認するというものです。

モデルの選択

私たちの経験では、RPC もしくは webhook を使ったコールバックを基本とする疎結合なモデルを使った継続的インテグレーションシステムは非常に実装しやすいものでした (ただし複雑なやり取りが必要となる厳密なビルド同士の連携は要件から外しています)。リモートでのチェックアウトとビルドの基本的な実行は、ビルドを制御するのがローカルであれリモートであれ同じような設計の制約を受け、ビルドに関する情報 (成功/失敗など) を収集するのはクライアントの仕事であり、アーキテクチャ全体やビルド結果に関する追跡情報を集めるときにもクライアントからの情報が必要になります。そのためレポートモデルを使えば、基本的な CI システムをとても簡単に実装可能です。

疎結合のモデルが非常にフレキシブルで拡張性に優れることも判明しました。要素同士がきちんと分離していて独立性が高いために、結果のレポート、通知メカニズム、ビルド指示書といった新しい機能を追加するのは簡単でした。要素が切り離されることで他の要素に行わせるタスクが明確になるので、要素のテストと変更がしやすくなります。

CDash のような疎結合のモデルを使ったリモートビルドにおいて唯一困難となるのが、ビルド同士の連携です。つまりビルドの開始と停止、現在行われているビルドに関するレポート、異なるクライアント間のリソースロックの調整は他の実装に比べて技術的に難しくなります。

以上を持って疎結合のモデルがどんな場合でも“良い”のだと結論するのは簡単ですが、この主張が成り立つのはビルド同士の連携が必要無いときだけであることを忘れてはいけません。決断を下すときには、プロジェクトが CI システムを使うときに必要となるものをよく考えるべきです。

未来

Pony-Build について考える間に、未来の継続的インテグレーションシステムに期待される機能がいくつか見えてきました。

最後に

これまでに紹介してきた継続的インテグレーションシステムは、アーキテクチャに合う機能を実装しています。またハイブリッドな Jenkins システムは最初マスター/スレーブモデルでしたが、疎結合のレポートアーキテクチャを後から追加しました。

以上の議論からアーキテクチャが機能を縛ると結論したくなりますが、もちろんそれは間違いです。アーキテクチャの選択が特定の種類の機能へと開発を導くようだ、と言うのが正しいでしょう。Pony-Build を通して私たちは、開発の最初に CDash スタイルのレポートアーキテクチャを選択したことが、後の設計と実装の判断に大きな影響を及ぼしたことに驚きました。いくつかの機能、例えば中央で管理される設定やスケジュールシステムが無いことは、Pony-Build の想定された使用例によって決定しました。私たちはリモートを動的に追加するという Buildbot で難しい機能を実装したかったのです。その他の機能、例えば進捗レポートや中央集権型のリソースロックは Pony-Build にあった方が良いのですが、実装されていません。これは機能が複雑なためにどうしても必要でない限り追加する気になれないためです。

同じような考えは Buildbot、CDash、Jenkins にも通用します。どのシステムにも、便利にも関わらずおそらくはアーキテクチャが適していないという理由で実装されていない機能が存在します。ただし Buildbot と CDash のコミュニティメンバーとの会話および Jenkins のウェブサイトを通じて明らかになったのは、最初に実装するべき機能が選ばれ、その機能が実装しやすいアーキテクチャを使ってシステムが作られたということです。例えば CDash のコミュニティは比較的少数のコア開発者からなり、CDash を中央集権的なモデルを使って開発しました。彼らが一番に考えていたのはコアとなるマシンでソフトウェアを動作させ続けることであり、その次がソフトウェアに精通したユーザーからバグレポートを受け取ることでした。また Buildbot は多数のクライアントからの共有リソースへのアクセスが調整されなければならない複雑なビルド環境での使用例が増えています。Buildbot の持つ柔軟な設定ファイルフォーマットを使えばスケジュール、変更の通知、リソースのロックを細かく指定できるので、複雑な設定が必要な場合には他のシステムよりも Buildbot が好まれています。それから Jenkins は使いやすくて単純な継続的インテグレーションシステムを目指しているようで、設定のための完全な GUI やローカルサーバーで実行するためのオプションなどがあります。

オープンソース開発の社会学も、アーキテクチャと機能の間のもう一つの交絡因子です。開発者がオープンソースプロジェクトを選ぶときには、そのプロジェクトのアーキテクチャが自身の使い道に合うかどうかで選ぶはずです。だとすれば、彼らが行うコントリビューションはこれまでのアーキテクチャで上手く行っているプロジェクトでの使い心地を改善するものになります。コントリビューターは自ら志願してなるものであり、機能が十分でないアーキテクチャを持つプロジェクトは避けられる可能性があるので、プロジェクトは特定の機能を集中的に使うようになるでしょう。これは私たちが Buildbot にコントリビュートすることなく Pony-Build という新しいシステムを実装した理由でもあります。Buildbot のアーキテクチャは数百あるいは数千のパッケージのビルドには使えないのです。

現在の継続的インテグレーションシステムは一般的に言って二つの異なるアーキテクチャのどちらかを使って作られており、望ましい機能の一部分しか実装されていないことが普通です。CI システムが成熟してユーザー数が大きくなれば機能が追加されるのだろうと思うかもしれませんが、実装されていない機能はアーキテクチャによる制限によって実装できないという場合もあります。今後の発展が興味深い分野と言えるでしょう。

謝辞

一般的な CI システムおよび Pony-Build について興味深い議論をしてくれた、Greg Wilson、Brett Cannon、Eric Holscher、Jesse Noller、Victoria Laidler に感謝します。また Jack Carlson、Fatima Cherkaoui、Max Laite、Khushboo Shakya を含む何人かの学生は、Pony-Build の開発に参加してくれました。



Amazon.co.jp アソシエイト (広告)
Audible の無料体験を始めよう
amazon music unlimited で音楽聞き放題