コンテンツへスキップ

QualiArtsengineer blog

ReSharper コマンドラインツールを活用したUnityプロジェクトの自動お掃除CI

ReSharper コマンドラインツールを活用したUnityプロジェクトの自動お掃除CI

9 min read

はじめに

株式会社QualiArtsでUnityエンジニアをしている住田です。 Unityのプロジェクトに従事し、並行して「CA.unity」や「技術書典」といった会社を跨いだ横軸活動の牽引などもしております。

昨今のUnityの開発現場ではIDEとしてJetBrains製のRiderが採用されるケースが増えています。弊社のUnityプロジェクトの開発でも積極的にRiderを導入しております。 そのIDEで提供されている機能と同等のことができるJetBrains製のReSharperというツールはご存知でしょうか。 本記事ではこのReSharperが提供しているコマンドラインツールの活用事例として、弊社のUnityプロジェクトで運用されているコードの整形を自動で行う仕組みについて紹介します。

Code Cleanup

Riderにはさまざまなコードのリファクタリング機能が搭載されています。 そのひとつがコードの整形機能であるCode Cleanupです。文法に沿った整形はもちろんのこと、参照のないusingの削除、不要な改行の削除、修飾子や型の自動修正、スコープや関数定義順序の整理などさまざまな観点からコードを整理する機能が項目別に用意されています。

Code Cleanup

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

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のプルリクエストとして生成するまでのフローを自動化しています。

手順はシンプルで、次のような手順で実行しています。

  1. プロジェクトのClone
  2. ReSharperのコマンドラインツールのインストール
  3. Unityのセットアップと起動 (Android)
  4. Code Cleanupの実行 (Android)
  5. Unityのセットアップと起動 (iOS)
  6. Code Cleanupの実行 (iOS)
  7. 実行差分からプルリクエストの生成

これらの手順を実行する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、つまりはコードの問題検出の機能でも利用できます。自動でそういった処理を走らせたい場合には便利ではないでしょうか。

こういった取り組みの一例が、みなさまの開発の一助になれば幸いです。

2018年にサイバーエージェントに新卒入社。その後、QualiArtsにて新規プロジェクトのUnityエンジニアとして開発に携わる。現在は、運用プロジェクトのUnityリードエンジニアとして従事。ゲーム・エンターテイメント事業部(SGE)全体のUnityの技術促進を目的とする横軸組織「Unityコミュニティ」の責任者としても活動している。