はじめに
Azure DevOpsは開発から運用までサポートするツールをひとまとめにしたサービスです。
今回はその中のAzure Pipelinesというツールの使い方を調べてみました。
公式ページによると、Azure Pipelinesの概要は次のとおりです。
あらゆる言語、プラットフォーム、クラウドに対応した CI/CD を使用して、ビルド、テスト、デプロイできます。GitHub や他の Git プロバイダーに接続し、継続的にデプロイすることができます。
Azure PipelinesはCI/CDをサポートしてくれるようです。
HoloLens2のビルド
今回、Azure Pipelines を調べようと思ったのは、
HoloLens2向けアプリをビルドする時の2つの問題を解決したかったためです。
まず、並行して他の作業ができないほど、ビルド中のマシンのパフォーマンスが低下します。
マシン性能や常住ツールにもよるでしょうが、経験された方は多いかと思います。
加えて、ビルドに要する時間が長いです。こちらも経験された方には共感していただけるかと思います。
解決策としては2つの案があります。
ビルドマシンを用意して遠隔操作するか、クラウド上でビルド環境を構築するかです。
Azure Pipelinesはどちらもフォローしています。
”Self-hosted Agent” : 前者をサポート。ビルドマシンに”Agent”をインストールすることでAzureサービス配下で遠隔操作ができる。
”Microsoft-hosted Agent” : 後者をサポート。Microsoftが予め用意したVMをビルド環境として利用する。
今回は手軽に試せる、Microsoft-hosted Agentを検証していきます。
公開プロジェクトなら10プロジェクト分、
非公開プロジェクトなら1800分/月分のビルドジョブを無料で実行可能なので、
まずは使い勝手を検証してみるのもいいでしょう。
設定手順
まずはMicrosoftのアカウントにログインして、Azure DevOpsの利用を開始します。

上記画像の赤枠を選択後、アカウントの認証処理を進めていきます。
次にプロジェクトを作成します。
プロジェクトの配下にPipeline等のサービスが配置されるイメージです。

プロジェクトが作成できたらPipelineを作成します。
下記画像の赤枠の箇所を選択すれば作成可能です。

Pipelineを作成できたら連携するバージョン管理ツールを選択します。
今回はGitHubで連携を進めていきます。

認証後にリポジトリを選択したらYAMLファイルの編集画面に遷移します。
MicrosoftがHoloLens2用のYAMLファイルのテンプレートを用意してくれています。
YAMLファイルを編集する前にUnity Tools for Azure DevOpsをプロジェクトにインストールします。
Unityに関するタスクが簡単に利用可能となるAzure DevOps向けに用意されたプラグインです。
右上の紙袋マークを選択し、遷移先のページで”Unity”と検索をかけます。


Get it freeを選択して次に進みます。

どのプロジェクトにプラグインをインストールするか選択し、Installを押します。

これでプロジェクトにUnityに関するタスクをYAMLファイルで定義して利用できるようになりました。
それでは、早速YAMLファイルを編集していきます。
下記がテンプレートを元に一部編集したYAMLファイルです。
# Variables predefined for this build process:
variables:
# If true, will run tests, otherwise skip them. If you do not have tests, set to false to increase build speed.
runTests: false
# The path to the folder which contains the Assets folder of the project.
# If your Unity project is located in a subfolder of your repo, make sure it is reflected in this.
unity.projectPath: '$(System.DefaultWorkingDirectory)/'
# If you are using Unity 2019 or later, leave this alone!
unity.installComponents: 'Windows, UWP'
# The build method of the Unity project. This assumes you have MRTK in your project, and uses its build script.
# If you want to customize your build script, change the method name here:
unity.executeMethod: 'Microsoft.MixedReality.Toolkit.Build.Editor.UnityPlayerBuildTools.StartCommandLineBuild'
# Are we buolding an .appx for x86 or ARM?
vs.appxPlatforms: 'ARM'
# I would not expect you to have to change the rest of these unless you had a special reason:
unity.targetBuild: 'WindowsStoreApps'
unity.outputPath: '/Builds/WSAPlayer'
unity.editorPath: '/Editor/Unity.exe'
vs.packagePath: '/AppPackages'
# This also needs to be passed to the install template, along with unity.projectPath
unity.installFolder: 'C:/Program Files/Unity/Hub/Editor/'
# What causes us to build? A push to master or a feature branch causes us to build...
trigger:
batch: true
branches:
include:
- master
- feature/*
# Windows machine with Visual Studio 2019:
pool:
vmImage: 'windows-latest'
# Two jobs in this pipeline:
# - Build the Unity
# - Run Unity tests
# Note: The build job can be uncommented and broken up into two jobs, for when that makes sense.
jobs:
# Install Unity (from cache or download) then create Visual Studio project from Unity
- job: unity
displayName: Unity Build
variables:
installCached: true
# Try to ensure that we have the right secrets set up to continue, otherwise fail the job:
condition: or( not(variables['unity.username']), not(variables['unity.password']), not(variables['unity.serial']) )
steps:
# What version of Unity does the project say that it wants?:
- task: UnityGetProjectVersionTask@1
name: unitygetprojectversion
displayName: Calling UnityGetProjectVersionV1 from unity-azure-pipelines-tasks extension
inputs:
unityProjectPath: '$(unity.projectPath)'
# TODO: This is the start of code that is repeated in other jobs, and ought to be done via a seperate file template.
# Do we have that Unity installation cached? If so, install from cache:
# (Note: The key is the hashed contents of the ProjectVersion.txt file)
# What is this? See https://docs.microsoft.com/en-us/azure/devops/pipelines/caching/index?view=azure-devops
- task: CacheBeta@0
displayName: Check if Unity installation is cached
inputs:
key: $(Agent.OS) | "$(unitygetprojectversion.projectVersion)" | "$(unity.installComponents)"
path: "$(unity.installFolder)$(unitygetprojectversion.projectVersion)"
cacheHitVar: installCached
# Install the Unity setup module (if we aren't cached):
- task: PowerShell@2
displayName: Install Unity
condition: and(succeeded(), ne(variables['installCached'], true))
inputs:
targetType: 'inline'
script: |
Install-Module -Name UnitySetup -AllowPrerelease -Force -AcceptLicense
# Download and run the installer for Unity Components defined in unity.installComponents:
- task: PowerShell@2
displayName: Installing Unity Components '$(unity.installComponents)'
condition: and(succeeded(), ne(variables['installCached'], true))
inputs:
targetType: 'inline'
script: |
Install-UnitySetupInstance -Installers (Find-UnitySetupInstaller -Version '$(unitygetprojectversion.projectVersion)' -Components $(unity.installComponents)) -Verbose
# Activate the Unity license (In theory, should deactivate the licence after use!):
- task: UnityActivateLicenseTask@1
displayName: Calling UnityActivateLicenseTask@1 from unity-azure-pipelines-tasks extension
inputs:
username: '$(unity.username)'
password: '$(unity.password)'
serial: '$(unity.serialkey)'
unityEditorsPathMode: 'unityHub'
unityProjectPath: '$(unity.projectPath)'
# Build the project with Unity using the script defined in unity.executeMethod:
- task: UnityBuildTask@3
displayName: Calling UnityBuildTask@3 from unity-azure-pipelines-tasks extension
name: runbuild
inputs:
buildScriptType: existing
scriptExecuteMethod: '$(unity.executeMethod)'
buildTarget: '$(unity.targetBuild)'
unityProjectPath: '$(unity.projectPath)'
outputPath: '$(Build.BinariesDirectory)'
# Publish the Solution folder:
- task: PublishPipelineArtifact@0
displayName: 'Publish Pipeline Artifact'
inputs:
artifactName: 'sln'
targetPath: '$(unity.projectPath)$(unity.outputPath)'
# Find, download, and cache NuGet:
- task: NuGetToolInstaller@1
displayName: 'Install NuGet'
# Restore the NuGet packages for the solution:
- task: NuGetCommand@2
displayName: 'NuGet restore'
inputs:
restoreSolution: '$(unity.projectPath)$(unity.outputPath)/*.sln' # Change to '$(unity.projectPath)$(unity.outputPath)/*.sln' to resume two jobs
# Build the solution with Visual Studio to make an .appx:
- task: MSBuild@1
displayName: 'Build solution'
inputs:
solution: '$(unity.projectPath)$(unity.outputPath)' # Change to '$(unity.projectPath)$(unity.outputPath)' to resume two jobs
configuration: Release
msbuildArguments: '/p:AppxBundle=Always /p:AppxBundlePlatforms="$(vs.appxPlatforms)"'
# Publish the package (.appxbundle/.msixbundle) that we just built:
- task: PublishPipelineArtifact@0
displayName: 'Publish Pipeline Artifact'
inputs:
artifactName: 'apppackages'
targetPath: '$(unity.projectPath)$(unity.outputPath)$(vs.packagePath)' # Change to '$(unity.projectPath)$(unity.outputPath)$(vs.packagePath)' to resume two jobs
# Build the Visual Studio solution generated from the previous job and create a package.
- job: unitytests
dependsOn: unity
condition: and(succeeded(), eq(variables['runTests'], 'true'))
displayName: Unity Tests
variables:
installCached: false
steps:
# What version of Unity does the project say that it wants?:
- task: UnityGetProjectVersionTask@1
name: unitygetprojectversion
displayName: Calling UnityGetProjectVersionV1 from unity-azure-pipelines-tasks extension
inputs:
unityProjectPath: '$(unity.projectPath)'
- task: CacheBeta@0
displayName: Check if Unity installation is cached
inputs:
key: $(Agent.OS) | "$(unitygetprojectversion.projectVersion)" | "$(unity.installComponents)"
path: "$(unity.installFolder)$(unitygetprojectversion.projectVersion)"
cacheHitVar: installCached
# Install the Unity setup module (if we aren't cached):
- task: PowerShell@2
displayName: Install Unity
condition: and(succeeded(), ne(variables['installCached'], true))
inputs:
targetType: 'inline'
script: |
Install-Module -Name UnitySetup -AllowPrerelease -Force -AcceptLicense
# Download and run the installer for Unity Components defined in unity.installComponents:
- task: PowerShell@2
displayName: Installing Unity Components '$(unity.installComponents)'
condition: and(succeeded(), ne(variables['installCached'], true))
inputs:
targetType: 'inline'
script: |
Install-UnitySetupInstance -Installers (Find-UnitySetupInstaller -Version '$(unitygetprojectversion.projectVersion)' -Components $(unity.installComponents)) -Verbose
# Activate the Unity license (In theory, should deactivate the licence after use!):
- task: UnityActivateLicenseTask@1
displayName: Calling UnityActivateLicenseTask@1 from unity-azure-pipelines-tasks extension
inputs:
username: '$(unity.username)'
password: '$(unity.password)'
serial: '$(unity.serialkey)'
unityEditorsPathMode: 'unityHub'
unityProjectPath: '$(unity.projectPath)'
# Play mode tests:
- powershell: |
Write-Host "======================= PlayMode Tests ======================="
$logFile = New-Item -Path .\playmode-test-run.log -ItemType File -Force
$proc = Start-Process -FilePath "$(unity.installFolder)$(unitygetprojectversion.projectVersion)$(unity.editorPath)" -ArgumentList "-projectPath $(unity.projectPath) -runTests -testPlatform playmode -batchmode -logFile $($logFile.Name) -editorTestsResultFile .\test-playmode-default.xml" -PassThru
$ljob = Start-Job -ScriptBlock { param($log) Get-Content "$log" -Wait } -ArgumentList $logFile.FullName
while (-not $proc.HasExited -and $ljob.HasMoreData)
{
Receive-Job $ljob
Start-Sleep -Milliseconds 200
}
Receive-Job $ljob
Stop-Job $ljob
Remove-Job $ljob
Stop-Process $proc
displayName: 'Run PlayMode tests'
# Publish test results:
- task: PublishTestResults@2
displayName: 'Publish Test Results'
inputs:
testResultsFormat: NUnit
testResultsFiles: 'test*.xml'
failTaskOnFailedTests: true
job: unityの中に記述のあるinstallCached: true と記述してある箇所は
テンプレートファイルにおいてfalseとなっています。
trueにしていないと、ビルドのたびにUnityをインストールし直すのでfalseにしておくことをお薦めします。
UnityActivateLicenseTask@1の箇所でライセンス認証を行っています。
username: '$(unity.username)'
password: '$(unity.password)'
serial: '$(unity.serialkey)'
ビルド時にライセンスをアクティベートしています。
この際、アクティベートするアカウントはUnity Pro ライセンスである必要があります。
アクティベート時には事前にPipeline上の編集画面で定義している変数を利用しています。
つまり、unity.username、unity.password、unity.serialkeyの3つの変数は前もって登録しておく必要があります。
変数の登録はPipeline上の編集画面からVariablesを選択して行います。

下記をそれぞれ登録します。シリアライズキーはUnity IDにログインして確認が可能です。
- unity.username ( Unity アカウントのメールアドレス )
- unity.password ( Unity アカウントのパスワード )
- unity.serialkey ( Untiy Plus/Pro のシリアライズキー )

ここまで適切に設定を終えてPipelineを実行した場合、
ビルドしたアプリを下記画像赤枠の箇所からダウンロードできます。

おわりに
Azure DevOpsのAzure Pipelinesを利用して、HoloLens2のCIまでを簡単に行うことができました。
冒頭で述べた下記の2つの問題点について結果を見ていきたいと思います。
・ビルド中のマシンのパフォーマンス
・ビルドに要する時間
まず、ビルド中のマシンのパフォーマンスについては
Microsoft-hosted Agentの利用により、開発マシンに影響無くビルドすることができています。
次に、ビルドに要する時間ですが、VM上でUnityエディターのインストールをキャッシュしたとしても
30分ほどビルドに時間がかかってしまい、通常のビルドと大差はありませんでした。
ビルド時間の短縮が課題として残りますが、
“Self-hosted Agent”と”Microsoft-hosted Agent”を組み合わせた高速化の事例もあるようなので
利用料金や規模感に合わせて最適解を選ぶとスマートなCI/CDライフを送れそうですね。