なつねこメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ 書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

Unity 2018.x で自動的に UPM パッケージをインストールしたい

BOOTH とかで商品を配布していて、共通の部分とかを配布するときに、 UPM 経由で依存を入れたいときとかの方法。
UnityPackage に含めると他のと被った場合、古いものをインポートされると困る!的な場合、
(個人的には) UPM 経由でインストールさせるのがよいと思っています。

しかし、その操作をユーザーに強制させたくないので、それを自動的に行う方法。
ただし、半ば無理矢理に。


前提条件は以下の通り:

  • Unity 2018.x
  • UPM 経由で Unity 公式ではないパッケージをインストールする
  • すべての操作においてユーザーの操作を含まない
  • スクリプト 1 つのコピペだけで完結する

ということで、まずは Unity 公式ではないパッケージの追加方法のおさらい。
基本的には、以下のような Scoped Registry セクションを追加してあげることで可能になります。

{
  "scopedRegistries": [
    {
      "name": "Mochizuki",
      "url": "https://registry.npmjs.com",
      "scopes": ["dev.mochizuki"]
    }
  ]
}

これを追加してあげることで、 dev.mochizuki.* なパッケージについては、
UPM を介して、 NPM に上げたパッケージをインストールされます。

また、この状態でのスクリプト経由での外部パッケージの追加は、以下のコードで可能です。

var request = UnityEditor.PackageManager.Client.Add("com.example.package");
while (!request.IsCompleted) { }

if (request.Error != null)
    Debug.LogError(request.Error.message);

ということから、自動でスクリプトからやってあげることは以下の 2 つです。

  1. Scoped Registries の追加・追記
  2. 対象のターゲットのインストール

ただし問題となる部分があり、 Unity ではまともに JSON を扱う手段がありません。
JsonUtility というものもありますが、いわゆる Dictionary<string, object> を扱えません。
ということで、無理矢理文字列操作で該当部分を挿入することにしたのが以下のコードです。

/*-------------------------------------------------------------------------------------------
 * Copyright (c) Natsuneko. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for license information.
 *------------------------------------------------------------------------------------------*/

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;

using UnityEngine;

namespace Mochizuki.ExtensionsLibrary.Examples
{
    [InitializeOnLoad]
    public static class DependencyInstaller
    {
        private const string SCOPE = "moe.mochizuki";
        private const string VERSION = "1.0.0";

        private static readonly Regex RegistryRegex = new Regex(@"^(?=.*""url"":\s+""https://registry.npmjs.com"")(?=.*""scopes"":\s+\[\s*""moe.mochizuki""\s*\])");
        private static readonly ListRequest ListRequest;
        private static readonly string ManifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");

        private static readonly string[] Dependencies =
        {
            "moe.mochizuki.extensions-library.editor",
        };

        static DependencyInstaller()
        {
            ListRequest = Client.List(true);
            EditorApplication.update += OnUpdate;
        }

        private static void OnUpdate()
        {
            if (!ListRequest.IsCompleted)
                return;

            EditorApplication.update -= OnUpdate;

            var localPackages = ListRequest.Result;

            foreach (var dependency in Dependencies)
                if (localPackages.All(w => $"{w.name}@{w.version}" != $"{dependency}@{VERSION}"))
                    Install($"{dependency}@{VERSION}");
        }

        private static void Install(string package)
        {
            if (!IsAlreadyRegisteredScope())
                InstallRegistry();

            InstallPackage(package);
        }

        private static bool IsAlreadyRegisteredScope()
        {
            var json = ReadManifest().Replace("\r", "").Replace("\n", "");
            if (json.Contains("scopedRegistries") && RegistryRegex.IsMatch(json))
                return true;

            return false;
        }

        private static void InstallRegistry()
        {
            var json = ReadManifest();
            var registry = @"
{
      ""name"": ""Mochizuki"",
      ""url"": ""https://registry.npmjs.com"",
      ""scopes"": [""moe.mochizuki""]
}".Trim();

            if (json.Contains("scopedRegistries"))
            {
                // Insert new scope to head of scopedRegistries
                var i = json.IndexOf("{", json.IndexOf("scopedRegistries", StringComparison.Ordinal), StringComparison.Ordinal);
                json = $"{json.Substring(0, i)}{Environment.NewLine}{registry},{Environment.NewLine}{json.Substring(i)}";
            }
            else
            {
                // Insert scopedRegistries section to head of JSON
                var section = $@"
  ""scopedRegistries"": [
    {registry}
  ],
".Trim();

                json = $"{{{Environment.NewLine}{section}{Environment.NewLine}{json.Substring(1)}";
            }

            using (var sw = new StreamWriter(ManifestPath))
                sw.WriteLine(json);
        }

        private static void InstallPackage(string package)
        {
            var request = Client.Add(package);
            while (!request.IsCompleted) { }

            if (request.Error != null)
                Debug.LogError(request.Error.message);
        }

        private static string ReadManifest()
        {
            using (var sr = new StreamReader(ManifestPath))
                return sr.ReadToEnd();
        }
    }
}

処理としては、

  1. 正規表現を用いて、該当するレジストリが登録されているかを確認
  2. 登録されていなければ、
    1. scopedRegistries がすでにある場合は追記
    2. scopedRegistries がない場合は追加
  3. 最後にパッケージのインストール

といったものを、文字列操作でやっています。疲れますね。
ということで、無理矢理インストールする方法でした、ではでは。