はじめに
株式会社QualiArtsでUnityエンジニアをしている住田です。 Unityのプロジェクトに従事し、並行して「CA.unity」や「技術書典」といった会社を跨いだ横軸活動の牽引などもしております。
昨今のUnityの開発現場ではIDEとしてJetBrains製のRiderが採用されるケースが増えています。弊社のUnityプロジェクトの開発でも積極的にRiderを導入しております。 そのIDEで提供されている機能と同等のことができるJetBrains製のReSharperというツールはご存知でしょうか。 本記事ではこのReSharperが提供しているコマンドラインツールの活用事例として、弊社のUnityプロジェクトで運用されているコードの整形を自動で行う仕組みについて紹介します。
Code Cleanup
Riderにはさまざまなコードのリファクタリング機能が搭載されています。 そのひとつがコードの整形機能であるCode Cleanupです。文法に沿った整形はもちろんのこと、参照のないusingの削除、不要な改行の削除、修飾子や型の自動修正、スコープや関数定義順序の整理などさまざまな観点からコードを整理する機能が項目別に用意されています。

言語別の設定や、プロジェクト固有で実行したい特定の項目だけの実行なども可能です。 実行する設定はCleanup Profileで項目ごとに制御可能です。プロジェクトに適用したい項目だけを有効化して利用できます。

このように自動かつ項目別でコードの整形を行えるため、一定の規模のプロジェクトについて開発を行う上で非常に便利な機能です。
ReSharperのコマンドラインツール
冒頭にもあるように、ReSharperとはJetBrains社が提供している、.NET開発環境向けの開発支援ツールです。 冒頭で紹介したRiderでも、ReSharperの機能と同等のものが提供されています。
.NETツールとして提供されており、コマンドラインからインストールできます。
dotnet tool install -g JetBrains.ReSharper.GlobalTools
インストールが完了すると、jbコマンドが実行できるようになります。このコマンドを通じて、提供されている各種機能を利用できます。
# Code Cleanupの呼び出し
jb cleanupcode Sample.sln {オプション}
# Inspect Codeの呼び出し
jb inspectcode Sample.sln {オプション}
Code Cleanupは前述したコードの整形機能で、Inspect Codeは文字通り静的コードの解析ツールです。 Inspect Codeについては本記事では深く触れませんが、コードの問題点をテキスト形式で出力する機能です。 Unityに即した機能も有しており、内部の機能について、問題のある使い方の箇所も解析してくれます。
詳しいコマンドラインツールの使い方は公式ドキュメントを参考にしてください。
GitHub Actions + ReSharperによる自動Code Cleanup
Code Cleanupは便利ですが、毎回エンジニアの手元で実行するのは面倒です。また、Code Cleanup自体の実行コストもあり、実装を変更するたびにちょっとした待機時間が発生してしまいます。 そこで、QualiArtsではReSharperのコマンドラインツールを活用し、定期的に自動でCode Cleanupを行う仕組みを導入しています。 具体的にはGitHub Actions上でCode Cleanupをコマンドラインツールから実行し、実行結果による差分をGitHubのプルリクエストとして生成するまでのフローを自動化しています。
手順はシンプルで、次のような手順で実行しています。
- プロジェクトのClone
- ReSharperのコマンドラインツールのインストール
- Unityのセットアップと起動 (Android)
- Code Cleanupの実行 (Android)
- Unityのセットアップと起動 (iOS)
- Code Cleanupの実行 (iOS)
- 実行差分からプルリクエストの生成
これらの手順を実行するGitHub ActionsのYAMLについて一部割愛したものを次に示します。
name: 'Code Cleanup'
inputs:
project-path:
description: 'プロジェクトパス'
required: false
default: './'
generate-solution-method:
description: 'ソリューション生成のために実行するメソッド'
required: true
cleanup-include:
description: 'Cleanup対象のパス'
required: true
cleanup-profile:
description: 'Cleanupのプロファイル'
required: true
cleanup-verbosity:
description: 'Cleanupのログ出力レベル'
required: false
default: 'WARN'
github-token:
description: 'GitHubのトークン'
required: true
add-paths:
description: 'コミット対象のファイルパス'
required: false
default: ''
runs:
using: "composite"
steps:
- name: Setup unity from project
uses: ./.github/quaunity-actions/setup-unity-from-project
with:
project-path: ${{ inputs.project-path }}
unity-modules: -m ios -m android
- name: Install Resharper Tool
run: |
dotnet tool install -g JetBrains.ReSharper.GlobalTools || true
echo "$DOTNET_ROOT/tools" >> $GITHUB_PATH
shell: bash
- name: Generate Solution(Android)
uses: ./.github/quaunity-actions/unity-execute-method
with:
build-target: Android
project-path: ${{ inputs.project-path }}
execute-method: ${{ inputs.generate-solution-method }}
- name: Cleanup
run: |
echo '::group::Cleanup'
PROJECT_PATH=${{ inputs.project-path }}
jb cleanupcode --profile="${{ inputs.cleanup-profile }}" \
--include=${{ inputs.cleanup-include }} \
--verbosity=${{ inputs.cleanup-verbosity}} \
${PROJECT_PATH%/}/*.sln
echo '::endgroup::'
shell: bash
- name: Generate Solution(iOS)
uses: ./.github/quaunity-actions/unity-execute-method
with:
build-target: iOS
project-path: ${{ inputs.project-path }}
execute-method: ${{ inputs.generate-solution-method }}
- name: Cleanup
run: |
echo '::group::Cleanup'
PROJECT_PATH=${{ inputs.project-path }}
jb cleanupcode --profile="${{ inputs.cleanup-profile }}" \
--include=${{ inputs.cleanup-include }} \
--verbosity=${{ inputs.cleanup-verbosity}} \
${PROJECT_PATH%/}/*.sln
echo '::endgroup::'
shell: bash
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ inputs.github-token }}
commit-message: code cleanup
branch: "hogehoge/code_cleanup"
title: Code Cleanup
body: |
GitHub ActionsによるCode Cleanupです
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
add-paths: ${{ inputs.add-paths }}
コマンドラインツールのインストールは .NET Tool を利用して行っています。
- name: Install Resharper Tool
run: |
dotnet tool install -g JetBrains.ReSharper.GlobalTools || true
echo "$DOTNET_ROOT/tools" >> $GITHUB_PATH
shell: bash
その次に対象のUnityのプロジェクトについて、C#のソリューションファイルを生成します。 ソリューションファイルを生成する理由は、Code Cleanupのコマンドラインツールがソリューション単位での指定を必要とするためです。 ソリューションファイルを生成するメソッド自体はプロジェクト内で実装し、それをバッチモードから呼び出します。
using Packages.Rider.Editor.ProjectGeneration;
namespace Sample
{
public static class DotnetProjectUtility
{
public static void CreateProject()
{
new ProjectGeneration().Sync();
}
}
}
なお、バッチモードで特定のメソッドを実行するGitHub Actionsは、社内基盤として用意されており、それを呼び出す形になっています。 プロジェクトによっては対象のソリューションファイルをカスタマイズできるよう、プロジェクト側から生成する形を取っています。 この社内基盤については後述します。
- name: Generate Solution(Android)
uses: ./.github/quaunity-actions/unity-execute-method
with:
build-target: Android
project-path: ${{ inputs.project-path }}
execute-method: ${{ inputs.generate-solution-method }}
そして、ソリューションファイルを生成した後に、それを対象としてCode Cleanupを実行します。 Code Cleanupの実行は、インストールされたコマンドラインツールから実行します。この時、前述したCodeCleanup Profileの指定も行います。 GitHub Actionsの実行引数としてCodeCleanup Profileの名前を受け取り、コマンドライン引数に指定します。 加えて、ログのレベルについてもverbosityオプションから指定します。
jb cleanupcode --profile="${{ inputs.cleanup-profile }}" \
--include=${{ inputs.cleanup-include }} \
--verbosity=${{ inputs.cleanup-verbosity }} \
${PROJECT_PATH%/}/*.sln
そして、ここまでのソリューションファイルのセットアップからCleanupまでの流れをiOSとAndroidのふたつのプラットフォーム指定で行います。 この理由は#if UNITY_ANDROID、#if UNITY_IOSなどで囲まれたプラットフォーム固有のプリプロセッサ ディレクティブを含むコードについては、実行されるプラットフォームの対象となるコードのみがCode Cleanupの対象となるからです。 そのため、少し手間はかかりますが、プラットフォームごとに実行しています。
以上の工程を経て、Code Cleanupの実行による差分が発生するので、それをプルリクエストにします。 差分が発生しても無視したい部分があるケースもあるため、コミット対象は引数から指定できる形をとっています。
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ inputs.github-token }}
commit-message: code cleanup
branch: "hogehoge/code_cleanup"
title: Code Cleanup
body: |
GitHub ActionsによるCode Cleanupです
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
add-paths: ${{ inputs.add-paths }}
最後に、これらのフローを組み合わせた具体的なGitHub ActionsのYAMLについても一部割愛して紹介します。参考になれば幸いです。
name: 'Code Cleanup'
inputs:
project-path:
description: 'プロジェクトパス'
required: false
default: './'
generate-solution-method:
description: 'ソリューション生成のために実行するメソッド'
required: true
cleanup-include:
description: 'Cleanup対象のパス'
required: true
cleanup-profile:
description: 'Cleanupのプロファイル'
required: true
cleanup-verbosity:
description: 'Cleanupのログ出力レベル'
required: false
default: 'WARN'
github-token:
description: 'GitHubのトークン'
required: true
add-paths:
description: 'コミット対象のファイルパス'
required: false
default: ''
runs:
using: "composite"
steps:
- name: Setup unity from project
uses: ./.github/quaunity-actions/setup-unity-from-project
with:
project-path: ${{ inputs.project-path }}
unity-modules: -m ios -m android
- name: Install Resharper Tool
run: |
dotnet tool install -g JetBrains.ReSharper.GlobalTools || true
echo "$DOTNET_ROOT/tools" >> $GITHUB_PATH
shell: bash
- name: Generate Solution(Android)
uses: ./.github/quaunity-actions/unity-execute-method
with:
build-target: Android
project-path: ${{ inputs.project-path }}
execute-method: ${{ inputs.generate-solution-method }}
- name: Cleanup
run: |
echo '::group::Cleanup'
PROJECT_PATH=${{ inputs.project-path }}
jb cleanupcode --profile="${{ inputs.cleanup-profile }}" \
--include=${{ inputs.cleanup-include }} \
--verbosity=${{ inputs.cleanup-verbosity}} \
${PROJECT_PATH%/}/*.sln
echo '::endgroup::'
shell: bash
- name: Generate Solution(iOS)
uses: ./.github/quaunity-actions/unity-execute-method
with:
build-target: iOS
project-path: ${{ inputs.project-path }}
execute-method: ${{ inputs.generate-solution-method }}
- name: Cleanup
run: |
echo '::group::Cleanup'
PROJECT_PATH=${{ inputs.project-path }}
jb cleanupcode --profile="${{ inputs.cleanup-profile }}" \
--include=${{ inputs.cleanup-include }} \
--verbosity=${{ inputs.cleanup-verbosity}} \
${PROJECT_PATH%/}/*.sln
echo '::endgroup::'
shell: bash
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ inputs.github-token }}
commit-message: code cleanup
branch: "hogehoge/code_cleanup"
title: Code Cleanup
body: |
GitHub ActionsによるCode Cleanupです
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
add-paths: ${{ inputs.add-paths }}
CIツールの基盤化
今回紹介したような汎用的なコードの整形を行うCIの仕組みは特定のプロジェクトに限らず必要なものです。 そこで、QualiArtsではGitHub Actionsで作ったCIの仕組みについては共通の基盤として管理し、複数のプロジェクトで活用できる形をとっています。 具体的には、Composite Actionとして社内の「quaunity-actions」という基盤ライブラリで管理し、それぞれのプロジェクトがGitHub Actionsで最低限の実装をするだけで仕組みを利用できるようになっています。 前述したCode CleanupのGitHub ActionsのYAMLはComposite Actionsとして記述されています。サンプルとして、それをプロジェクト側で利用する際のGitHub ActionsのYMALを紹介します。
# プロジェクト側のサンプル
name: Cleanup Sample
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
jobs:
build:
runs-on: [self-hosted]
steps:
- uses: actions/checkout@v4
- name: Code Cleanup
uses: ./.github/quaunity-actions/cleanup
with:
project-path: "hogehoge"
cleanup-include: "Assets/**/*.cs"
cleanup-profile: "Actions: CustomReformat Code"
add-paths: "Assets"
本記事で紹介したCode Cleanup以外にも、アプリのビルドなどの仕組みがquaunity-actionsを通して各プロジェクトに提供されています。それらについても、プロジェクト側は最低限の実装で利用することが可能です。 GitHub Actionsの活用については、過去のブログで弊社の田村が「UnityプロジェクトにおけるGitHub Actions活用」と題して詳しく紹介しています。ぜひあわせてご覧ください。
UnityプロジェクトにおけるGitHub Actions活用
おわりに
ReSharperのコマンドラインツールを活用することで、普段RiderなどのIDEで利用しているコード整形などの機能をCIで利用できます。これを活用することで、 ReSharperのコマンドラインツールでは、今回紹介したCode Cleanupだけではなく、Code Inspection、つまりはコードの問題検出の機能でも利用できます。自動でそういった処理を走らせたい場合には便利ではないでしょうか。
こういった取り組みの一例が、みなさまの開発の一助になれば幸いです。