勉強不足で至らんブログ

勉強不足ですが色々と書いていきます。

トラッキング技術で遊んでLTしてました

最近遊んでた全身トラッキングについてまとめて発表してました。 Unityの具体的な実装とかではなく、これが楽しかった!とかになります。

主に

github.com

になります。

AppleのM1 mac minifps計測してみたり口パクも追加してみたりなど

随所に動画を入れてますがGoogle Slideみたいに指定した箇所からの再生がうまくいかないので飛び飛びに観たりしてもらえればと思います。

VroidStudioで作った顔と体をBlenderで合成する

BelnderでVroidの顔と体を合わせさせたい!

ということで自分はVroidStudioというツールを使って3Dモデルを作ろうとしました。最初女性アバターで制作して後から男性の体にしたいと思ったのですが、そういう機能は標準になかったのでBlenderで編集したという備忘録です。

BlenderVRMを扱う場合はこちらの記事を参考にセットアップしてみてください。 【Blender】VRMファイルを扱う 特にBlenderのバージョンに注意してください。

以下はサンプルで

サンプルのVroidの以下の女性アバターf:id:MakeTake:20210116190352p:plain

新規作成で出来る男性アバターです(多少手を加えてますが気にしないで大丈夫です) f:id:MakeTake:20210116190410p:plain

VRMプラグインのセットアップが完了したBlenderで操作を行います。

新規でBlenderのプロジェクトを作成します f:id:MakeTake:20210116190843p:plain

右にあるCubeってところは消しましょう。

VroidStudioを使って.vrmフォーマットにエクスポートをします。(男女それぞれ)

まずは女性アバターをimportしましょう f:id:MakeTake:20210116191518p:plain

色が付いてないのでわかりづらいですね、色をつけましょう! f:id:MakeTake:20210116191612p:plain 右上の赤で囲っているところをクリックすれば色がつきます(バージョンによって違うかもしれません)

次に男性アバターもimportします f:id:MakeTake:20210116191804p:plain

身長の差が生まれました…が一旦今日は無視します。 女性アバターのデフォルト身長は150cmくらいで男性アバターf:id:MakeTake:20210116191919p:plain ってやると揃いますので参考程度にお願いします。 顔と体を入れ替える際にはVroidStudioの方でそろえておくと簡単です。

今回は適当に辻褄を合わせます

適当な命名をしてるので名前は気にしないでほしいですが男性/女性アバターをimportすると以下のような Scene Collection となります f:id:MakeTake:20210116192042p:plain

ここから女性アバターBody.baked を消して男性アバターBody.baked を移動させます

  1. まずは女性アバターBody を消します f:id:MakeTake:20210116193834g:plain

  2. 男性アバターからBodyを移植してBoneを当てなおします(スカートとかのが残ってますが適宜調整してください) f:id:MakeTake:20210116194116g:plain

これで f:id:MakeTake:20210116194821p:plain

首が離れていると思います。

これを少しだけ修正しましょう f:id:MakeTake:20210116195050p:plain

f:id:MakeTake:20210116195106p:plain

顔(Face)と髪(Hair)に当たるところのZ軸を -0.05 しておきます(適宜調整しましょう)

f:id:MakeTake:20210116195156p:plain

いい感じになりましたね

これをエクスポートします

Unityにimportするとこんな感じですね f:id:MakeTake:20210116195812p:plain

UnityChanのアニメーションを適当に再生してみます f:id:MakeTake:20210116200429g:plain

動いてますね!!!

ヨシッ!!!!!!!

GitHub ActionsについてLTしてました

GitHub Actionsを使ってみた話を社内LTで話してました。

完全版ではないです。完全版には具体的な定義方法などがわかるようにしています。完全版が見たい方は入社すると見れます。

UPMでsubmoduleが追加されないのでsubmodule部分を補填するEditor拡張作ってみた

Unity Advent Calendar 2020の記事です

Unity #3 Advent Calendar 2020 の2日目の記事になります。

Unity#3まであるのすごいですね

Unity Package Managerは便利

Unity Package ManagerでAssetsディレクトリ配下がスッキリするようになってかなり助かっています。そして openupm も使ったりしています。

最近 VRM を使っていて VroidStudio を使ってモデルを作ったのでVRMモデルで遊んでいます。 そこで自分のUnityProjectでVRMモデルを入れて UniVRM を使って制御をしたりしています。

UniVRMは標準のUPMで入れようとしたときにうまくいかなかったのでopenupmで導入しています。

そして、この記事はかなり力技で厳密にsubmoduleのコミット持ってくるとかせずに最新のものを適応させたりしてるのでご了承ください。

以下がリポジトリ

github.com

UniVRMをopenupmで導入したらエラーが出る

f:id:MakeTake:20201114231820p:plain

MToon というものがないそうです。これでは動かせないので解決していきます。 UnityのProejctで確認しても空のディレクトリとなっていました。

f:id:MakeTake:20201114231655p:plain

GitHubを確認するとsubmoduleで追加されたディレクトリのようです。

f:id:MakeTake:20201114232249p:plain

ということはupmはsubmoduleのディレクトリは落とさないようになっているのでしょう。自分で追加する可能性もあるのでそれはそうって感じですね。

Git URL からのインストール という公式に乗っ取りMToonを追加しようとすると

f:id:MakeTake:20201118201220p:plain

エラーがでました。upmに対応してないようです。なので今回はEditor拡張を作って解決したいと思ったので作りました。

submoduleを持ってくればええやんけ

シンプル単純愚直に考えてsubmoduleをgitで引っ張てくれればええやろと思いました。

Packageのコンテンツは Library/PackageCache/ 配下に羅列されます。

f:id:MakeTake:20201114233221p:plain

当初この各Packageの下には .git があってgitで操作できると思ってましたがありません。が、それでもいいので .gitmodule というsubmoduleを管理しているファイルを引っ張れればまだいいと思いましたが消し去られていました。( .gitattribute なども無くなっていたのでgit周り全部取り除かれているみたいです )

そこでファイルを漁っているとupm経由になったことでREADME.mdが追加されており開くと

# VRMShaders

VRM model's supported shaders in Unity.

## Import VRMShaders (Unity 2019.3.4f1~)

`Window` -> `Package Manager` -> `Add package from git URL` and paste `https://github.com/vrm-c/UniVRM.git?path=/Assets/VRMShaders`.

or add the package name and git URL in `Packages/manifest.json`:

{
  "dependencies": {
    "com.vrmc.vrmshaders": "https://github.com/vrm-c/UniVRM.git?path=/Assets/VRMShaders",
  }
}

もろjsonでurlが記載されていたのでREADME.mdから取得するようにしました。

これでGitHubで.gitmoduleを確認してsubmoduleのリポジトリをzipで落として突っ込もうと思います。

実装

実装なんて知らない!ってお方は次の 使い方 まで飛ばしてください。

PackageSubmoduleDownloader.cs(全体)

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace PackageSubmoduleDownloader
{
    public class PackageSubmoduleDownloader
    {

        private static float progress;
        private static float per = 1;
        private static readonly List<string> submoduleDirectories = new List<string>();

        private static string UnityDirectoryPath = $"{Application.dataPath}/..";
        private static string PackageCachePath => $"{UnityDirectoryPath}/Library/PackageCache";
        private static string TempDirectoryPath = $"{UnityDirectoryPath}/Temp";

        [MenuItem("Assets/Package Submodule Downloader")]
        public static async Task Execute()
        {
            AssetDatabase.DisallowAutoRefresh();
            progress = 0;
            UpdateProgressBar(0.1f, nameof(Execute));
            await DownloadSubmoduleAsync();
            AssetDatabase.Refresh();
            UpdateProgressBar(1 - progress, "Finish");
            AssetDatabase.AllowAutoRefresh();
            EditorUtility.ClearProgressBar();
        }
        
        private static void UpdateProgressBar(float addProgress, string info)
        {
            if (progress + addProgress > 0.9) addProgress = 0;
            progress += addProgress;
            Debug.Log($"[{nameof(PackageSubmoduleDownloader)}] {info} : {progress}");
            EditorUtility.DisplayProgressBar(nameof(PackageSubmoduleDownloader), info, progress / per);
        }
        
        public static async Task DownloadSubmoduleAsync()
        {
            var directories = Directory.GetDirectories(PackageCachePath);
            per = directories.Length;
            foreach (var directory in directories)
            {
                submoduleDirectories.Clear();
                SearchEmptyDirectory(directory);
                var isExitsSubModule = submoduleDirectories.Count != 0;
                if (!isExitsSubModule) continue;
                UpdateProgressBar(0.3f, nameof(GetGitHubUrl));

                var githubUrl = GetGitHubUrl(directory);
                var repositoryName = githubUrl.Split('/').Last();
                var downloadPath = $"{TempDirectoryPath}/{repositoryName}";
                var gitmoduleString = await GitSubmodulesAsync(githubUrl, downloadPath);
                if (gitmoduleString == "") continue;
                UpdateProgressBar(0.3f, nameof(ParseUrls));

                var submoduleUrls = ParseUrls(gitmoduleString);
                var per = submoduleUrls.Count();
                foreach (var url in submoduleUrls)
                {
                    var directoryName = url.Split('/').Last();
                    var tempDirectory = $"{TempDirectoryPath}/{directoryName}";
                    await DownloadRepositoryZipAsync(url, $"{TempDirectoryPath}/{directoryName}");
                    UpdateProgressBar(0.1f / per, nameof(DownloadRepositoryZipAsync));
                    foreach (var subModuleDirectory in from subModuleDirectory in submoduleDirectories let isTargetDirecotry = subModuleDirectory.Contains(directoryName) where isTargetDirecotry select subModuleDirectory)
                    {
                        await UnZipAsync($"{tempDirectory}.zip");
                        UpdateProgressBar(0.1f / per, nameof(UnZipAsync));
                        var source = Directory.GetDirectories(tempDirectory).First();
                        DirectoryMove(source, subModuleDirectory);
                    }
                    UpdateProgressBar(0.1f / per, nameof(DirectoryMove));
                }
            }
        }
        
        private static void SearchEmptyDirectory(string path)
        {
            foreach (var directory in Directory.GetDirectories(path))
            {
                SearchEmptyDirectory(directory);
                var isEmptyFiles = Directory.GetFiles(directory).Length == 0;
                var isEmptyDirectories = Directory.GetDirectories(directory).Length == 0;
                if (isEmptyFiles && isEmptyDirectories)
                {
                    submoduleDirectories.Add(directory);
                }
            }
        }
        
        private static string GetGitHubUrl(string path)
        {
            var readmeFileName = $"{path}/README.md";
            if (!File.Exists(readmeFileName)) return "";
            using (var raedmeFile = new StreamReader(readmeFileName))
            {
                var readme = raedmeFile.ReadToEnd().Split('\n');
                var urlLine = readme.FirstOrDefault(x => x.StartsWith("    \"com."));
                if (urlLine == null) return "";
                // e.g.)    "com.example.package": "https://github.com/example/package",
                return urlLine.Split('"')[3].Split('?').First().Replace(".git", "");
            }
        }

        private static async Task<string> GitSubmodulesAsync(string url, string downloadPath)
        {
            var gitmoduleUrl = $"{url}/master/.gitmodules".Replace("github.com", "raw.githubusercontent.com");
            var isExistsGitModules = await IsExistsGitSubmodulesAsync(gitmoduleUrl);
            if (!isExistsGitModules) return "";
            Directory.CreateDirectory(downloadPath);
            var client = new HttpClient();
            var response = await client.GetAsync(gitmoduleUrl);
            return await response.Content.ReadAsStringAsync();
        }

        private static async Task<bool> IsExistsGitSubmodulesAsync(string url)
        {
            var client = new HttpClient();
            var response = await client.GetAsync(url);
            try
            {
                response.EnsureSuccessStatusCode();
            }
            catch
            {
                // ignored
            }
            return response.IsSuccessStatusCode;
        }

        private static IEnumerable<string> ParseUrls(string gitmoduleString)
        {
            var fileValue = gitmoduleString.Split('\n');
            return fileValue.Where(x => x.StartsWith("  url =")).Select(x => x.Replace("  url = ", "").Replace(".git", ""));
        }

        private static async Task DownloadRepositoryZipAsync(string url, string downloadPath)
        {
            var completionSource = new TaskCompletionSource<TaskStatus>();
            using (var client = new WebClient())
            {
                var lastVersion = await GetLastTagAsync($"{url}/releases/latest/download");
                client.DownloadFileCompleted += (sender, args) =>
                {
                    completionSource.SetResult(TaskStatus.RanToCompletion);
                };
                client.DownloadFileAsync(new Uri($"{url}/archive/{lastVersion}.zip"),  $"{downloadPath}.zip");
            }
            await completionSource.Task;
        }

        private static async Task<string> GetLastTagAsync(string url)
        {
            var client = new HttpClient();
            var response = await client.GetAsync(url);
            try
            {
                response.EnsureSuccessStatusCode();
            }
            catch
            {
                // ignored
            }
            return response.RequestMessage.RequestUri.ToString().Split('/').Last();
        }
        
        private static Task<int> UnZipAsync(string zipPath)
        {
            var completionSource = new TaskCompletionSource<int>();
            var contentName = zipPath.Replace(".zip", "").Split('/').Last();
            var outputPath = $"{TempDirectoryPath}/{contentName}";
#if UNITY_EDITOR_WIN
            var unityEditorDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
            var sevenZipPath = $"{unityEditorDirectory}/Data/Tools/7z.exe";
            var fileName = $"\"{sevenZipPath}\"";
            var arguments = $"x \"{zipPath}\" -o\"{outputPath}\" -r";
#else
            var fileName = "unzip";
            var arguments = $"\"{zipPath}\" -d \"{outputPath}\"";
#endif
            if (Directory.Exists(outputPath)) Directory.Delete(outputPath, true);
            var process = new Process
            {
                StartInfo =
                {
                    FileName = fileName,
                    Arguments = arguments,
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true
                },
                EnableRaisingEvents = true
            };
            
            process.Exited += (sender, args) =>
            {
                completionSource.SetResult(process.ExitCode);
                process.Dispose();
            };
            process.OutputDataReceived += (sender, args) => {
                if (!string.IsNullOrEmpty(args.Data)) {
                    Debug.Log(args.Data);
                }
            };
            process.ErrorDataReceived += (sender, args) => {
                if (!string.IsNullOrEmpty(args.Data)) {
                    Debug.LogError(args.Data);
                }
            };
            process.Start();
            return completionSource.Task;
        }
        
        private static void DirectoryMove(string sourceDirectory, string destDirectory)
        {
            var sourceInfo = new DirectoryInfo(sourceDirectory);

            var directories = sourceInfo.GetDirectories();
            Directory.CreateDirectory(destDirectory);
            
            var files = sourceInfo.GetFiles();
            foreach (var file in files)
            {
                var destFileName = Path.Combine(destDirectory, file.Name);

                File.Move(file.FullName, destFileName);
            }

            foreach (var directory in directories)
            {
                var dest = Path.Combine(destDirectory, directory.Name);
                DirectoryMove(directory.FullName, dest);
            }
        }
    }
}

分割してコメントを入れて解説とさせてもらいます。

        // Editorから実行するメソッド
        [MenuItem("Assets/Package Submodule Downloader")]
        public static async Task Execute()
        {
            // 処理が終わるまでEditorの更新がかからないようにする
            AssetDatabase.DisallowAutoRefresh();
            progress = 0;
            UpdateProgressBar(0.1f, nameof(Execute));
            await DownloadSubmoduleAsync();
            AssetDatabase.Refresh();
            UpdateProgressBar(1 - progress, "Finish");
            // 処理が終わるのでEditorの更新をかけていいようにする
            AssetDatabase.AllowAutoRefresh();
            EditorUtility.ClearProgressBar();
        }
        
        // 適当にプログレスバーを出しておく
        private static void UpdateProgressBar(float addProgress, string info)
        {
            if (progress + addProgress > 0.9) addProgress = 0;
            progress += addProgress;
            Debug.Log($"[{nameof(PackageSubmoduleDownloader)}] {info} : {progress}");
            EditorUtility.DisplayProgressBar(nameof(PackageSubmoduleDownloader), info, progress / per);
        }
        
        public static async Task DownloadSubmoduleAsync()
        {
            // UPMの配置場所の取得
            var directories = Directory.GetDirectories(PackageCachePath);
            per = directories.Length;
            foreach (var directory in directories)
            {
                submoduleDirectories.Clear();
                // 空のディレクトリがあればsubmodule候補として列挙する
                SearchEmptyDirectory(directory);
                var isExitsSubModule = submoduleDirectories.Count != 0;
                if (!isExitsSubModule) continue;
                UpdateProgressBar(0.3f, nameof(GetGitHubUrl));

                // README.mdからGitHubのURLを取得する
                var githubUrl = GetGitHubUrl(directory);
                var repositoryName = githubUrl.Split('/').Last();
                var downloadPath = $"{TempDirectoryPath}/{repositoryName}";
                // GitHubのURLから最新の.submoduleを読み取る
                var gitmoduleString = await GitSubmodulesAsync(githubUrl, downloadPath);
                if (gitmoduleString == "") continue;
                UpdateProgressBar(0.3f, nameof(ParseUrls));
                
                // .submoduleに定義されてるurlを列挙する
                var submoduleUrls = ParseUrls(gitmoduleString);
                var per = submoduleUrls.Count();
                foreach (var url in submoduleUrls)
                {
                    var directoryName = url.Split('/').Last();
                    var tempDirectory = $"{TempDirectoryPath}/{directoryName}";
                    // submoduleの最新のzipをUnityのTempにダウンロードする
                    await DownloadRepositoryZipAsync(url, $"{TempDirectoryPath}/{directoryName}");
                    UpdateProgressBar(0.1f / per, nameof(DownloadRepositoryZipAsync));
                    // submoduleの名前と空のディレクトリの名前を突き合わせて合致していたら処理を行う
                    foreach (var subModuleDirectory in from subModuleDirectory in submoduleDirectories let isTargetDirecotry = subModuleDirectory.Contains(directoryName) where isTargetDirecotry select subModuleDirectory)
                    {
                        // ダウンロードしたzipをunzipする
                        await UnZipAsync($"{tempDirectory}.zip");
                        UpdateProgressBar(0.1f / per, nameof(UnZipAsync));
                        var source = Directory.GetDirectories(tempDirectory).First();
                        // tempディレクトリからUPMのディレクトリに移動させる
                        DirectoryMove(source, subModuleDirectory);
                    }
                    UpdateProgressBar(0.1f / per, nameof(DirectoryMove));
                }
            }
        }

以降はメモっておきたいメソッドをピックアップしていきます

        private static string GetGitHubUrl(string path)
        {
            var readmeFileName = $"{path}/README.md";
            if (!File.Exists(readmeFileName)) return "";
            using (var raedmeFile = new StreamReader(readmeFileName))
            {
                var readme = raedmeFile.ReadToEnd().Split('\n');
                // urlのある行はこういう文字列から始まるという力技検索
                var urlLine = readme.FirstOrDefault(x => x.StartsWith("    \"com."));
                if (urlLine == null) return "";
                // こういう文字列が絶対くる前提というの力技処理
                // e.g.)    "com.example.package": "https://github.com/example/package",
                return urlLine.Split('"')[3].Split('?').First().Replace(".git", "");
            }
        }

次はunzip処理

        private static Task<int> UnZipAsync(string zipPath)
        {
            var completionSource = new TaskCompletionSource<int>();
            var contentName = zipPath.Replace(".zip", "").Split('/').Last();
            var outputPath = $"{TempDirectoryPath}/{contentName}";
#if UNITY_EDITOR_WIN
            // Unity.exeの場所を取得してUnity.exe周りにある7-Zipのexeを取得してzip解凍に利用する
            var unityEditorDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
            var sevenZipPath = $"{unityEditorDirectory}/Data/Tools/7z.exe";
            var fileName = $"\"{sevenZipPath}\"";
            var arguments = $"x \"{zipPath}\" -o\"{outputPath}\" -r";
#else
            // unzipはmac/linux系にはあるでしょって前提で書いたもの Unity.app配下にも7z.aとかあったけどlinuxと共通にしたかったのでスルー
            var fileName = "unzip";
            var arguments = $"\"{zipPath}\" -d \"{outputPath}\"";
#endif

Windowsで解凍処理どうしようかなと思ってましたがUnityEngineに付属するならええやろ!って感じで利用しています。 以前にもWebGLのビルドツールにPython環境が丸ごと入っていたりと利用できる環境があったりするので漁ってみるのもおすすめです。 → Unity WebGLのBuild and RunのRunを実行する拡張Pythonについては記載

Unityのバージョンアップで使えなくなる可能性はあるので永続的な保証はありません。

特筆する処理はこれくらいだと思います。

使い方

"com.mizotake.packagesubmoduledownloader": "https://github.com/MizoTake/PackageSubmoduleDownloader.git?path=Assets"

manifest.json に追記すれば追加できます。

Assets/PackageSubmoduleDownloader がメニューに追加されるので

f:id:MakeTake:20201116191340p:plain

実行

たぶん以下のようなエラーがでるので Force Quit します。一度Editorを落としましょう。

f:id:MakeTake:20201118202634p:plain

そしてEditorを開きなおしてください。

f:id:MakeTake:20201116183258p:plain

たぶん MToon が入っていると思います。もし入っていない場合はもう一度 Assets/PackageSubmoduleDownloader を実行するとエラーなく入ると思います。

まとめ

今回Editorを再起動させない方法を色々考えましたがわかりませんでした。自前で.metaのGUID追加して認識するかな?とか思いましたが「思ったより沼が深かった」案件だったので断念して再起動の手をとっています。 ちなみに ReImport All などをすると今回のEditor拡張で追加したディレクトリは消えるので再度同じ手順をとる必要がでてきます。

何かと力技ですが自分の一番の目的は MToon を自分で入れなくてよくなったので満足です。汎用的に書いてるつもりですが何かと穴はありそうです。

こんな力技なアプローチがあるんだな程度にとって頂くといいと思います。

記事を書いて数日後に思ったのですがMtoonをforkしてpackage.json追加したらupmで入れられたのでは…いやはや…いやはや…

github.com

勝手ながらforkして package.json 追加いたしました、これPR投げていいのかなぁとOSS慣れしてないのでわかないので自分はこれでMToonを使わせて頂きます🙏

初めてOSSにissueを立てた

タイトル通り

magentaというプロジェクトの中にあるnote-seqというリポジトリにissueを立てました

どういう問題だったのかというと、Pythonの仕様上WindowsではTempディレクトリのファイルを開いた状態でコピー操作などができずエラーが出ます。そのエラーがでる処理が note-seq にあったということです。

すごく簡単そうな問題ですがWindowsで動かないのは自分が困ったのでissueを立てました。

github.com

すると即コメントが返ってきていてOSSのスピード感にビビりました。

そしてissueを立てたのは夜だったので寝ました。起きて対応PRが上がっていることに再びビビる。

対応PRが上がっているのに気づいたのが会社だったので帰って動作確認したところちゃんと動作しました。すごい。

実際に手を動かして問題を解決したわけではないですが自分きっかけで問題が修正されたのは純粋にありがたくうれしかったです。OSSに問題を報告するの大事だなと…いつかPR対応してみたいと思えた。

fastlaneでWindowsのmsbuildを使う

fastlaneでmsbuildを使いたい!

docs.fastlane.tools

fastlaneというモバイル向けのビルドステップを簡略化してくれるruby製のツールがあります。プラグインなどが簡単に導入でき柔軟でとてもよいツールです(SlackやCDツールとの連携が楽)

fastlaneは数年前までmacOS専用のiOSのみのツールでしたがWindows/Linuxにも対応し、Androidのサポートも加わっています。使い方によってはモバイルに限りません。

今回はモバイルであってもなくてもVisualStudioで開発する際にMSBuildコマンドが使えればfastlaneで多少楽にビルドステップが書けると思います。

そんなツールでmsbuildを使いWindowsプラットフォームでビルドする想定のメモ書きです。

msbuildを使うには

fastlaneはmsbuildの実行をデフォルトではサポートしていません。 fastlaneはshの実行も簡単にできるので何でもshで書けます。msbuildも全てshにまとめていると実行できます(gitbashやcygwin導入時)

ただ、shで実行するだけではfastlaneライクには書けません。

ここでプラグインの導入を検討しました。

github.com

検索をすると上記のプラグインが最初に目につくと思います。 自分もこれでmsbuildが使える!と思い中身を確認したところ、fastalneが元々macOSのみ対応していた名残もあり

https://github.com/willowtreeapps/fastlane-plugin-msbuild/blob/master/lib/fastlane/plugin/msbuild/actions/msbuild_action.rb#L9

この行を見ると引数で実行バイナリのディレクトリを指定するようになっています。引数がなければ msbuild というPATHの通ったバイナリを実行します。 これはMSBuildの構造の問題もありますが、MSBuidlのexeと同じディレクトリには MSBuild というディレクトリがあります。そのため、実行バイナリのディレクトリを指定するやり方では MSBuild ディレクトリを参照してしまい実行できません。そして MSBuild.exe にパスを通していてもプラグインmsbuild を呼び出すため実行されません。Windows環境では詰みました…

というわけで改めて探すと

github.com

というのを見つけました。Xamarinは内部的にmsbuildを使うところがあるので中身を確認したところ。引数で msbuild の実行バイナリを指定する形式でしたのでexeも指定できました!

これで一旦めでたし!なのです!が!

msbuildのコマンドオプションの引数が…ない…だと…

という事態に陥ったのですが、fastlaneの作りとして裏でターミナルなどにコマンドを渡しているだけではあるので

lane :release do
  option = " /m"
  msbuild(
    msbuild: "exeまでのパス",
    project: "slnまでのパス",
    target: "ビルドするターゲット" + option 
  )
end

のようにオプションのパラメータではない引数の後ろに空白を入力してオプションを指定すれば動きます!解決!(本当はissue作ってPRを作ったりした方がいいと思いますが)

おわり

fastlaneのmsbuildメモでした。Windows対応などがまだ浸透していないことを考えるとmacOSのみでビルド環境を構築した方がいいのかもしれません。ただ自分のようにWindowsでやりたい!という人は工夫をすれば乗り越えれると思います! rubyは割と簡単に扱えるので自分でプラグインを書くのもいいかもしれませんね!

追記(2020/12/01)

上記で紹介しているlaneを使うとログが正常に出ない場合がありshのlaneでMSBuild.exeを叩いた方がよさそうでした。ログが出るため。