はじめに
先日 Package Manager 対応のプルリクをいただいたきました。恥ずかしながら今まで Package Manger の対応およびその調査はサボってきており、あまり知識がない状態でしたので、これを機に色々と調べて配布する側としての自分なりの利用方針を決めていこうと思いました。以下、Unity Package Manager を省略して UPM と書きます(公式では一部を除き*1あまり UPM という表記は使われていないようですが、開発者の間では普通に使われている印象です)。
本エントリでは、既に色々配布しているライブラリがある上での立場から UPM についての調査を行った内容をまとめます。また、その上で自分は GitHub Actions を使ったパッケージのリリースを行う方針に決めたのですが、その道筋や具体的なコードを紹介していきます。
UPM の基本
UPM そのものについては既に世に色々と解説がありますので、本記事では基本のところ(使い方など)の解説は省きます。公式にとても分かりやすいドキュメントがありますので、そちらを参照いただくのをおすすめします。
簡単に述べると、これまで .unitypackage や直接インポートしたりしながら Assets 以下に整理して置いていた依存ライブラリが、Package Manager という仕組みを通じて、GUI を通じてライブラリを簡単に追加・削除でき、それらのバージョン管理が適切に行われ、依存関係が適切に解決され、またグローバルキャッシュを通じて異なるプロジェクト間でアセットが共有され、...といった具合に色々と便利になります。数々のアップデートを通じて、かなり使いやすいものになったようです。
他の言語やプラットフォームではパッケージ管理の概念は以前から存在しています。例えば Node.js では npm(Node Package Manager)というパッケージ管理システムを通じて、ライブラリの管理を行うことが出来ます。ちなみに UPM のバックエンドでも、この npm を利用しているようです。npm はインストールや管理だけでなくレジストリ(npmjs)も持っており、各開発者が作成したライブラリを npm publish
してホストしてもらえるのですが、Unity でもこの npmjs にライブラリを置いても良い?みたいです。
Package Managerに自作パッケージを追加する2019年版|fuqunaga|note
また、非公式レジストリである OpenUPM などもあり Project Settings から Scoped Registry を登録すると Package Manager から見えるようになります。
パッケージのソースとしては、このようなレジストリベースのものの他に以下のようなものにも対応しています。
- ビルトイン
- Unity 公式の機能
- 埋め込み
- Packages ディレクトリに自分で入れたもの
- ローカル
- PC 上の任意のフォルダ
- ローカル tarball
- 特殊用途
- Git
Unity が提供するものだけでなく、PC 上の任意のフォルダや Packages ディレクトリも読めるのでコードの依存関係を疎にしたりチーム開発する上でも便利そうですね。また、加えて Git が利用できるようになったのはライブラリ開発者・利用者双方から便利で素晴らしいです。また後で解説しますがリポジトリ上の任意のディレクトリを指定できる仕組みになっています。
どういったソースかはパッケージ名の右に表示されるのでわかりやすいです(ローカルだと Custom、Git だと git などのラベル)。
このようにとても便利になった UPM ですが、ライブラリ開発者側は気をつける点がたくさんあります。次にそれらを見ていきましょう。
UPM 対応の基本
パッケージ作成についての基本は以下のドキュメントにまとまっています。
パッケージ名
パッケージ名を決める必要があり、これには 2 つの表記があります。一つは表示用で、今までのように例えば拙作のものであれば uRaymarching、uREPL、uDesktopDuplication といったような名前がそれにあたります。もう一つは正式名で、逆ドメイン表記で表されるものです。com.hecomi.uraymarching、com.hecomi.urepl といったようなものになります。日本語のドキュメントだと「com.<company-name>」から始まるものにすると書いてありますが、英語のドキュメントを見ると、別に com に限らずドメインなら何でも良いようです。
ディレクトリ構造
次にディレクトリ構造を大きく変える必要があります。
<root> ├── package.json ├── README.md ├── CHANGELOG.md ├── LICENSE.md ├── Third Party Notices.md ├── Editor │ ├── [company-name].[package-name].Editor.asmdef │ └── EditorExample.cs ├── Runtime │ ├── [company-name].[package-name].asmdef │ └── RuntimeExample.cs ├── Tests │ ├── Editor │ │ ├── [company-name].[package-name].Editor.Tests.asmdef │ │ └── EditorExampleTest.cs │ └── Runtime │ ├── [company-name].[package-name].Tests.asmdef │ └── RuntimeExampleTest.cs ├── Samples~ │ ├── SampleFolder1 │ ├── SampleFolder2 │ └── ... └── Documentation~ └── [package-name].md
ライブラリとしての情報が記述されたパッケージマニフェストファイル package.json
と、このディレクトリ構造のいくつかのファイルから情報をパースし、Package Manager がうまく管理してくれるようになる感じです。これまでは Scripts
などのディレクトリの下に置かれることの多かったスクリプトは Runtime
以下に置くようにし、asmdef を合わせて配置する形にします。Editor
以下にエディタ用のスクリプトと asmdef を置くようにすることでそれぞれの分離がきれいになります。サンプルは Samples~
以下に配置し、どのようなサンプルがあるかを package.json
に記述します。こうすると利用者側は必要なサンプルのみ Package Manager 上でポチポチしてインポートできるようになります。
package.json
パッケージの名前やバージョン、作者、依存関係などの情報をここに記述します。
内容としては次のようなものになります。
{ "name": "com.unity.example", "version": "1.2.3", "displayName": "Package Example", "description": "This is an example package", "unity": "2019.1", "unityRelease": "0b5", "dependencies": { "com.unity.some-package": "1.0.0", "com.unity.other-package": "2.0.0" }, "keywords": [ "keyword1", "keyword2", "keyword3" ], "author": { "name": "Unity", "email": "unity@example.com", "url": "https://www.unity3d.com" } }
これはローカルパッケージなら GUI 上で編集が可能です。
library
や module
、tool
などのタイプを指定できる type
だけは何をどういう基準で選択すればよいかの説明がまだないので自分は空にしています。。
配布
さて、こうしてディレクトリ構造を整理し、package.json や関連するファイルを追加した後、GitHub など何らかの Git のリポジトリに公開します。このリポジトリの URL を利用者側に Package Manager 上で入力してもらう形になります。
ただ、Unity のプロジェクトとして Git にあげているプロジェクトの場合は package.json
がルートではなく Assets/ 以下に置かれていることもあると思います。以下は uOSC の例です:
このような場合は、次のように package.json
の置かれた場所を示す URL にするとインポートできます:
Git リポジトリに置く以外にも冒頭で述べたように npmjs や OpenUPM に登録すると、更新があった際にユーザが GUI 上で更新ボタンを押して更新できるようになります。これはまた別途検証します。
さて、これまでの情報でプロジェクト全体のディレクトリ構造を次に考えていきたいと思います。
開発リポジトリのディレクトリ構造
Assets に含める方式
先述のプロジェクトの構造を見てみます。
<Repository> ├── Assets │ └── uOSC │ ├── Examples │ └── Scripts │ ├── package.json │ ├── uOSC.asmdef │ ├── uOscClient.cs │ ├── ... ├── ...
この構造の問題点は Examples がディレクトリの外側にありパッケージに含められていない点です。これをパッケージにサンプルとして含めようとすると次のようになります。
<Repository> ├── Assets │ └── uOSC │ ├── package.json │ ├── Runtime │ │ ├── uOSC.asmdef │ │ ├── uOscClient.cs │ │ └── ... │ └── Samples~ │ ├── Sample 1 │ ├── Sample 2 │ ├── Sample 3 │ └── ... ├── ...
これで package.json
に Sample 1~3 のディレクトリを記述すればサンプルとしてポチポチとインポートできるようになります。しかしながらこの構造ではディレクトリ名にチルダがついていることから Unity のインポートから除外されてしまうため、Unity の Project ウィンドウからこれらサンプルが見えなくなってしまいます。これはライブラリ開発側として困りますし、.meta も生成されないので、一度チルダを外して調整、コミット前にチルダを追加...、などとやる必要が出てきてしまいます。面倒ですしリネームし忘れなどの事故も起きそうですね。
パッケージのみ管理方式
パッケージ部分のみ管理する方式を採用しているプロジェクトもあります。
これをローカルパッケージとして取り込み利用、外側のプロジェクトは自身の PC 上で開発、という方式ですね。自分はネイティブプラグインの開発プロジェクトも含むケースが多く、Packages の外側にそれを置きたいのでこの方式はしないかな、と判断しました。
Packages からシンボリックリンク方式
どうしようか困っていたら Twitter でご助言をいただきました。
これみたいにPackagesに本体を配置してAssetsにsymlinkを張ると開発しやすいですよhttps://t.co/RAa2CSd0sX
— KOGA Mitsuhiro (@shiena) 2021年10月16日
この方式にすると以下のような構造になります。
<Repository> ├── Packages │ └── com.hecomi.uosc │ ├── package.json │ ├── Runtime │ │ ├── uOSC.asmdef │ │ ├── uOscClient.cs │ │ └── ... │ └── Samples~ ├── Assets │ │ └── uOSC ↓ │ └── Samples [~Samples] │ ├── Sample 1 │ ├── Sample 2 │ ├── Sample 3 │ └── ... ├── ...
チルダを含まない形で Assets にサンプルのディレクトリをインポートできるので、適切に .meta も作成されます。また、ローカル方式でパッケージを作成すると、Project ウィンドウから直接パッケージ内のファイルを修正したり、新たにファイルを追加したりも出来るので通常通り開発が可能です。ただシンボリックリンクを使うと Unity から警告メッセージが表示されます。
Assets/uOSC/Samples is a symbolic link. Using symlinks in Unity projects may cause your project to become corrupted if you create multiple references to the same asset, use recursive symlinks or use symlinks to share assets between projects used with different versions of Unity. Make sure you know what you are doing.
利用者側にはこれは表示されないですし、このケースでは衝突もないので無視してしまってもよいかと思います。
ただ、Package による配布と従来の .unitypackage の配布を両立したい、となってくると問題が出てきます。レガシーな環境や Package Manager を使わない開発をしていたり、インポートしたあとに色々とスクリプトを修正して使いたい人は、以前と同じく .unitypackage で直接プロジェクトに取り込みたいと思うかもしれません(実際はローカルパッケージ化すれば編集できます)。しかしながらこの構造にしてしまうと .unitypackage が作りづらくなってしまいます。.unitypackage は Packages 以下のものを含めてパッケージングすることは出来ないからです。.unitypackage を配布しない場合はお手軽でかなり良い選択肢だと思います。
GitHub Actions で配布用のブランチを作成する方式
こちらの記事ではかなり進んだ内容が紹介されてました。
GitHub Actions を使ってメインブランチにコミットがあった際、そのコミットメッセージに応じて自動的にバージョンを付与、upm ブランチへ git subtree split
を使って必要なディレクトリのみ切り出し必要に応じて README.md
の移動や Samples~
へのリネームも行い、更に npmjs のリリースまで行うというものです。
自分のパッケージ作成方針
これまでの調査を踏まえて自分なりの構造を考えていきます。自分のやりたいことは以下のような形です:
- UPM での動作を確認するため、コアはローカルパッケージの形式で Packages 以下から見える形にしたい
- Samples は UPM でインポートする形式にしたいが、開発時のプロジェクトでは開発しやすいように普通にプロジェクトに含まれるようにしたい
- UPM 使わない人のために .unitypackage を簡単に作れるようにしたい
- できれば 1 リポジトリ、1 プロジェクトで開発したい
- ネイティブプラグインの開発も同じリポジトリで管理したい
- README.md は 1 つにしたい(GitHub のプロジェクトのルートでの表示用と、パッケージのルートディレクトリの配置用を分けたくない)
前項のいずれの方法を使っても、全部同時に実現するのは難しそうです(今は思いつきませんでした)。色々と検討したのですが、最後の GitHub Actions で自動的に UPM 用のリリースブランチを作成し、そこに必要な構造を作成する方式がとても良さそうに思えました。
元記事ではセマンティックバージョニング含めかなりしっかり作られていますが、自分は結構ゆるく運用してるので次のような感じで行こうかと思います。
- 1(ローカルパッケージとして開発)を諦める
- Runtime / Editor / Samples といったフォルダ構成を利用する(Samples~ にはしない)
- GitHub へバージョン付きの Tag を push した時に、自動的にパッケージ用の構造を作成する(
git subtree
で必要なファイルだけ切り出す & Samples を Samples~ へリネームするなど - OpenUPM や npmjs へのリリースは取り敢えず様子見
これにより、開発は次のような構造にします。
<Repository> ├── README.md ├── Assets │ └── uOSC │ ├── package.json │ ├── Runtime │ │ ├── uOSC.asmdef │ │ ├── uOscClient.cs │ │ └── ... │ └── Samples │ ├── Sample 1 │ ├── Sample 2 │ ├── Sample 3 │ └── ... ├── ...
ディレクトリ構造だけ変えておきます。.unitypackage はいつもどおり Unity 上で適当なタイミングで手動で作成します。
タグを push したときはそれをイベントとして次のようなブランチを作成します。作成したブランチは upm ブランチには最新を、また upm/v1.2.3 のようにバージョン付きのものも同時にリリースして古いバージョンを使いたい人は使えるようにします。ブランチの構造は git subtree
で切り出した上記 uOSC ディレクトリ内のファイル及び必要なファイル(README.md
や package.json
)を配置するようにします。
<Repository> ├── README.md ├── package.json ├── Runtime │ ├── uOSC.asmdef │ ├── uOscClient.cs │ └── ... ├─ Samples~ │ ├── Sample 1 │ ├── Sample 2 │ ├── Sample 3 │ └── ... ├── ...
GitHub Actions の作成
環境セットアップ
GitHub Actions の記事は、様々な業種で使われる関係上たくさんの解説がされていますし、公式のドキュメントも充実しています。
GitHub 上で開発するのは大変なので、自分は act を使ってローカルでワークフローのテストを行いました。
動作には Docker が必要ですが、昨今は Windows でも Docker Desktop for Windows のセットアップがポチポチするだけで簡単に終わるので導入は難しくありませんでした。
GitHub Actions の作成
次のようなアクションを作成します。
name: Update-UPM-Branch # v1.2.3 のようなタグがプッシュされたら起動 on: push: tags: - v* env: MAIN_BRANCH: main UPM_BRANCH: upm PKG_ROOT_DIR: Assets/uOSC SAMPLES_DIR: Samples DOC_FILES: README.md CHANGELOG.md LICENSE.md jobs: test: runs-on: ubuntu-latest steps: # 最新を取得 - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - run: git checkout main # イベントを起動したタグを steps.tag.outputs.name に格納 - name: Tag name id: tag run: echo ::set-output name=name::${GITHUB_REF#refs/tags/v} # Git の設定 - name: Git config run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # UPM 用のブランチを作成 - name: Create UPM branches run: | # 古いブランチを削除 git branch -D $UPM_BRANCH &> /dev/null || echo $UPM_BRANCH branch is not found # Assets/uOSC 以下を upm ブランチに切り出す git subtree split -P "$PKG_ROOT_DIR" -b $UPM_BRANCH # 切り出したブランチに移動 git checkout $UPM_BRANCH # メインブランチの方にあった README などをインポート for file in $DOC_FILES; do git checkout $MAIN_BRANCH $file &> /dev/null || echo $file is not found done # Samples ディレクトリを Samples~ に改名 git mv $SAMPLES_DIR Samples~ &> /dev/null || echo $SAMPLES_DIR is not found # Samples.meta だけ別途インポートされるので不要(他の .meta は必要) rm Samples.meta # package.json のバージョンを置換 sed -i -e "s/\"version\":.*$/\"version\": \"$TAG\",/" package.json || echo package.json is not foundあ # タグ名とともにコミット git commit -am "release $TAG." # GitHub へ push git push -f origin $UPM_BRANCH # タグ付きのブランチも作成して push git checkout -b $UPM_BRANCH@$TAG git push -f origin $UPM_BRANCH@$TAG env: TAG: ${{ steps.tag.outputs.name }}
これで例えば v1.2.3 を push すると、自動的に upm ブランチと upm@1.2.3 ブランチが作成され、これを https://github.com/hecomi/uOSC.git#upm
といった URL で Package Manager 上でインポートできるようになります。
meta の追加
ただ、まだ 1 点だけ問題があります。それは README.md
の meta ファイルが存在していないので、README.md
がインポートされない点です…。
Asset Packages/com.hecomi.uosc/README.md has no meta file, but it's in an immutable folder. The asset will be ignored.
ちょっとお行儀が悪いですが、README.md.meta
は package.json.meta
の UUID だけ変えて生成する形にしてしまいましょう。
... for file in $DOC_FILES; do git checkout $MAIN_BRANCH $file &> /dev/null || echo $file is not found # .meta ファイルを作成(package.json.meta を改変) if [ -f $file ]; then cp package.json.meta $file.meta UUID=$(cat /proc/sys/kernel/random/uuid | tr -d '-') sed -i -e "s/guid:.*$/guid: $UUID/" $file.meta git add $file.meta fi done ...
これで警告もなくインポートができるようになりました。そのうち Samples~
も自動生成するとかもやりたいです。
Composite Actions 化
さて、1 つのプロジェクトならこれで良いですが複数のプロジェクトでこのコードを全部コピペするのはちょっとしんどいです。バグが見つかったときも直して回る必要があるのも微妙です。
そこで、Composite Action を使ってこの一連の流れを使い回せるようにしてみようと思います。
先程のコードの env
を inputs
にして外から与えられるようにし、bash スクリプト部分はシェルスクリプトのファイルとして分離しました。この Composite Action は以下のように別のリポジトリを用意しておきます。
この上でそれぞれのリポジトリ側のアクションを次のように修正します。
name: Update-UPM-Branch on: push: tags: - v* jobs: update: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Tag name id: tag run: echo ::set-output name=name::${GITHUB_REF#refs/tags/v} - name: Create UPM Branches uses: hecomi/create-upm-branch-action@main with: git-tag: ${{ steps.tag.outputs.name }} pkg-root-dir-path: Assets/uOSC
とても短くなりました!あとはこのアクションを各リポジトリに同じように設定していけば同じコードを使って tag を push するだけでデプロイ出来るようになります。同じ仕組みでやりたい方がいらっしゃいましたらご活用ください。
設定例
本記事のコードでもいくつか出てきた uOSC で使い始めてみました。
無事動き始めています。
その他
パッケージ管理用のスクリプトが書ける
Unity.PackageManager.Client
を使うと特定のパッケージをプロジェクトに追加したり、パッケージのソースごとに列挙したりといったことが可能です。
上記ページではより進んだサンプルとして、ローカルでないパッケージをプロジェクトの Packages フォルダに取り込み、編集可能にするためのスクリプトが紹介されています。
おわりに
Package Manager について調べてみましたが、想定していたよりも便利で、自分のプロジェクトではアセットはすべてこれで管理したほうが良さそうだなぁ、と思うようになりました。同じように思う人のためにも、ポチポチと時間を見つけて配布しているライブラリをメンテしていこうと思います。また、今回は UPM 用のリリースブランチを作成するところまででしたが、後々 OpenUPM への登録、unitypackage の作成も自動化していきたいなぁと思っています。