Package のコードを無理矢理書き換える方法をご紹介(ただし macOS に限る)
はじめに
この記事は Unityゲーム開発者ギルド Advent Calendar 2022 (その1) | 13日目の記事になります。
前日の記事は アキオ さんの 簡単なメタバースを作ってみた でした。
本稿では「Packages の内容を無理矢理書き換えて、バグに対する急場凌ぎをする(ただし macOS に限る)」ためのテクニックをご紹介します。
ぶっちゃけ、このテクニックが必要になる人はかなり少ないと思いますが、1人でもこの記事が刺さってくれる人がいたら嬉しいです。
背景
環境とか
筆者が勤める株式会社キッズスターが開発中の新プロダクトでは、テキストのローカライズを行うに際して Localization パッケージを用いており、バージョンの組み合わせとしては以下のような組み合わせで開発を進めていました。
- Unity: 2022.1.24f1
- Localization: 1.4.2
開発中のプロジェクトに関して、Unity のバージョンは基本的に TECH ストリームの最新を用いるようにしており、特別な理由がない限り新しいパッチバージョンがリリースされる度にプロジェクトで用いる Unity のバージョンを上げる運用をしています。
発生した問題
ある日、新バージョンが降ってきたので、いつものように Unity のバージョンをあげてビルドを投げたところ、iOS ビルド中に次のようなエラーが発生していることが判明しました。
AmbiguousMatchException: Ambiguous match found
深く調査したところ、Localization パッケージに含まれる PosprocessBuild の処理内に PBXBuildFileData.CreateFromFile()
というメソッドを Reflection 経由で実行している箇所がありました。
で、ある時点まではこのメソッドにオーバーロードは存在せず、 typeof(PBXBuildFileData).GetMethod("CreateFromFile", BindingFlags.Static | BindingFlags.Public)
といった感じに BindingFlags の指定だけで一意に特定できていました。
が、Unity 2022.1.16f1 あたりから、PBXBuildFileData.CreateFromFile()
のオーバーロードが増え、引数型の組み合わせを指定しないと一意に特定できない状態になってしまいました。
この問題自体は Unity さんも認識しており、Issue Tracker に起票されているので、遠からず直る見込み(本稿執筆時点で In Review となっており、Localization v1.5.0 で直るっぽい)ではあります。
遠からず直るとはいえ、iOS でローカライズできなくなるのは困るので、なんらかの対策を講じる必要はあります。
考えられる対処方法
さて、このような「プラットフォームや 3rd Party のコードに起因する問題」に対する打ち手としては以下のようなものが考えられます。
- アップデートを待つ
- 問題が発生しないバージョンに下げる
- 力業でゴリ押しする
で、私は「待てないし、バージョン下げたくもない」というワガママな子なので、3.
のゴリ押しパターンを採択し、何とか問題をクリアできたので、本稿ではその手法について紹介します。
本題
今回、私が採択したゴリ押し方法は「問題を引き起こしているプログラムを無理矢理直す」という、ある意味シンプルなものになります。
しかしながらこの方法は、言うは易し行うは難しという感じで、幾つかの壁を乗り越える必要があるので、それぞれの壁と乗り越え方について詳説します。
壁1: 書き換えるモノの選定
今回、問題を引き起こしていたのは Package としてプロジェクトに組み込んでいるものでした。
Unity Package Manager 経由でインストールしたパッケージ(Embed や Local 参照のものを除く)は Library/PackageCache/
以下にソースコードが展開されます。
今回の例で言えば、 Library/PackageCache/com.unity.localization@1.4.2/Editor/Platform/iOS/PBXProjectExtensions.cs
というファイルが該当します。
これを下記のように書き換えることができれば万事解決!というわけです。
@@ -92,7 +92,7 @@
s_GuidListAdd = s_GUIDList.GetMethod("AddGUID");
s_GUIDListContains = s_GUIDList.GetMethod("Contains");
s_FileRefDataCreateFromFile = fileRefData.GetMethod("CreateFromFile", BindingFlags.Static | BindingFlags.Public);
- s_PBXBuildFileDataCreateFromFile = s_PBXBuildFileDat.GetMethod("CreateFromFile", BindingFlags.Static | BindingFlags.Public);
+ s_PBXBuildFileDataCreateFromFile = s_PBXBuildFileDat.GetMethod("CreateFromFile", new Type[] { typeof(string), typeof(bool), typeof(string)});
s_ProjectBuildFilesAdd = typeof(PBXProject).GetMethod("BuildFilesAdd", pv);
s_ProjectFileRefsAdd = typeof(PBXProject).GetMethod("FileRefsAdd", pv);
s_ProjectBuildFilesGetForSourceFile = typeof(PBXProject).GetMethod("BuildFilesGetForSourceFile", pv);
壁2: コンパイルのタイミング
じゃあ、「ビルド時とかに書き換えればええやん」と思って BuildPipeline.BuildPlayer()
実行直前に当該ファイルを書き換えてみたのですが、問題は解決しませんでした。
こちらの原因の深追いはしていないのですが、ソースコードの書き換えによって発動する再コンパイルや Assembly の再読み込みが間に合わない所為だと思われます。
ビルドのプロセスとは異なる流れで書き換えを行えれば良いのですが、ビルドサーバなどで batchmode による起動を行う場合、呼び出すメソッドは1つしか指定できないので、この調整が少し難しくなってきます。
こちらは、[InitializeOnLoad]
か [InitializeOnLoadMethod]
を用いて Editor 初期化時に発動する処理の中で書き換えを行うことで対応可能です。
[InitializeOnload]
public static class FixLocalization
{
static FixLocalization()
{
ReplaceCode();
}
private static void ReplaceCode()
{
// 正規表現による置換
}
}
壁3: Unity Package Manager による復元処理
壁2
の対応が済んだことで「よし、これで完璧!」とか思ってビルドを投げてみたところ、ログファイルには次のような警告が出力されていました。
The package cache was invalidated and rebuilt because the following immutable asset(s) were unexpectedly altered:
Packages/com.unity.localization/Editor/Platform/iOS/PBXProjectExtensions.cs
ざっくり言えば「なんか書き変わってたから戻しといたよ!」という Unity Package Manager さんの優しさが壁として立ちはだかったのです。
こちらに関して「んーーー、これは難しいぞ…。」と頭を抱えながら Google 先生に聞いてみたところ、matatabi-ux さん の下記の記事がヒットしました。
こちらの記事では、当該ファイルを Finder からロックすることで「Unity Package Manager による復元を無効化」しています。
同等の処理は chflags
コマンドに uchg
を渡すことでも実現できるので、以下のようなスクリプトを追加することで無事に復元を無効化することに成功しました。
private static PackageInfo PackageInfo { get; } = PackageInfo.FindForAssembly(typeof(LocalizationSettings).Assembly);
private static string ScriptFilePath { get; } = $"Library/PackageCache/{PackageInfo.packageId}/Editor/Platform/iOS/PBXProjectExtensions.cs";
private static void LockFile()
{
var process = new Process();
process.StartInfo = new ProcessStartInfo(
"/usr/bin/chflags",
$"uchg '{ScriptFilePath}'"
);
process.Start();
process.WaitForExit();
}
で、そもそも chflags
は macOS のコマンドであり、Windows の場合は別の方法でロックする必要があります。
が、筆者はここ10数年リンゴのマークがついたマシンしか使っていないので、Windows でのロックの掛け方については検証していません 🙇♂️
壁4: お掃除 🧹
これで「今度こそ完璧!」と思ったのですが、弊社のビルドシステムには「過去N世代分のビルド環境を残して、ところてん式に古いものを削除する」という仕組みが備わっており、数世代ビルドを重ねたのちに「なんか古いビルドを削除できないんだけど?」というエラーが発生するようになりました。
それもそのはずで、ロックしたファイルをそのままにしているので、「ビルドシステムが削除しようにも当該ファイルのみ削除できない」という状況に陥っていたのです。
まぁ、これは「ビルド終了時にアンロックする」だけで解決する話ではあるのですが、ビルドの処理内や IPostprocessBuildWithReport.OnPostprocessBuild(BuildReport report)
で実行してしまうと、ビルド中に別のエラーが発生した際にアンロックが行われなくなる可能性があります。
なので、「エディタのプロセスが終了する時」である EditorApplication.quitting
にアンロック処理を実行するのが確実性が高いと言えます。
[InitializeOnLoad]
public static class FixLocalization
{
static FixLocalization{}
{
EditorApplication.quitting += UnlockFile;
ReplaceCode();
LockFile();
}
// (略)
private static void UnlockFile()
{
var process = new Process();
process.StartInfo = new ProcessStartInfo(
"/usr/bin/chflags",
$"uchg '{ScriptFilePath}'"
);
process.Start();
process.WaitForExit();
}
}
これにて「Localization の修正が来るまでの無理矢理ワークアラウンド」が完成です 🎉
まとめ
というわけで「Package 側のバグを無理矢理修正する方法」についてまとめると、次のようになります。
- 対象のファイルを探し出し、ソースコードを書き換えるスクリプトを書く
-
[InitializeOnLoad]
とかで書き換えスクリプトを実行する -
chflags
コマンドを用いてファイルをロックする - エディタ終了時にファイルをアンロックする
本稿では Localization のバグに言及した内容となっていますが、他の Package でもこの対応を当てはめることが可能な場合があります。
「アプデを待つ以外に方法がないが、ちょっとしたコードの修正で回避できるバグ」を踏んだ際に、本稿の手法を試してみては如何でしょうか?
なお、本稿で扱った内容を1つのスクリプトとしてまとめたものを Gist に置いておきましたので、よければ参考にしてみてください。
明日の Unityゲーム開発者ギルド Advent Calendar 2022 (その1) は 鉄 さんの 使用ライブラリを簡単に覗く方法~Windows編~ です。
Discussion