Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/audience-build-budget.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"note": "Baselines and limits must be set after the first run on main (workflow_dispatch). Set baselineBytes to the measured size, maxBytes to a comfortable ceiling (e.g. measured + 10 MB). A maxBytes of 0 means no limit is enforced for that platform.",
"platforms": {
"Android": { "baselineBytes": 0, "maxBytes": 0 },
"iOS": { "baselineBytes": 0, "maxBytes": 0 },
"Windows": { "baselineBytes": 0, "maxBytes": 0 },
"macOS": { "baselineBytes": 0, "maxBytes": 0 }
}
}
5 changes: 3 additions & 2 deletions .github/scripts/audience/matrix-shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
{ "target": "StandaloneLinux64", "runner": "ubuntu-latest-8-cores", "install_unity_script": "", "run_playmode_script": ".github/scripts/audience/playmode-linux.sh" }
],
"mobile_targets": [
{ "target": "Android", "build_player_method": "AndroidBuilder.Build" },
{ "target": "iOS", "build_player_method": "IosBuilder.Build" }
{ "target": "Android", "build_player_method": "AndroidBuilder.Build" },
{ "target": "iOS", "build_player_method": "IosBuilder.Build" },
{ "target": "StandaloneWindows64", "build_player_method": "WindowsBuilder.Build" }
],
"pr_exclude": [
{ "unity": "2022.3.62f2" },
Expand Down
247 changes: 244 additions & 3 deletions .github/workflows/test-audience-sample-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -266,23 +266,264 @@ jobs:
if-no-files-found: ignore
path: |
examples/audience/Builds/Android/*.apk
examples/audience/Builds/Windows/**
examples/audience/Logs/**

# iOS build size: Unity batch mode produces the Xcode project, xcodebuild compiles
# to a real .app without signing. No Apple certs needed for size measurement.
build-size-ios:
needs: paths-changed
if: |
github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.fork == false
&& needs.paths-changed.outputs.audience == 'true'
name: Build size / iOS
runs-on: ["self-hosted", "macOS", "ARM64"]
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
lfs: true

- name: Cache Unity Library (iOS size)
uses: actions/cache@v4
with:
path: examples/audience/Library
key: Library-size-iOS-2021.3.45f2-${{ hashFiles('examples/audience/Assets/**', 'examples/audience/Packages/**', 'examples/audience/ProjectSettings/**', 'src/Packages/Audience/**') }}
restore-keys: Library-size-iOS-2021.3.45f2-

- name: Install Unity
env:
UNITY_VERSION: 2021.3.45f2
UNITY_CHANGESET: 88f88f591b2e
BACKEND: Mono2x
run: .github/scripts/audience/install-unity-macos.sh

- name: Install iOS build support module
env:
UNITY_VERSION: 2021.3.45f2
UNITY_CHANGESET: 88f88f591b2e
run: |
"/Applications/Unity Hub.app/Contents/MacOS/Unity Hub" -- --headless install-modules \
--version "$UNITY_VERSION" --changeset "$UNITY_CHANGESET" --architecture arm64 \
--module ios \
|| echo "(install-modules non-zero, OK if 'No modules found to install')"

- name: Build Xcode project
run: |
"$UNITY_PATH" \
-projectPath "${{ github.workspace }}/examples/audience" \
-executeMethod Immutable.Audience.Samples.SampleApp.Editor.IosBuilder.Build \
-buildTarget iOS \
-logFile "${{ github.workspace }}/ios-unity-build.log" \
-quit -batchmode

- name: Compile with Xcode (no signing)
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
run: |
xcodebuild \
-project "${{ github.workspace }}/examples/audience/Builds/iOS/Unity-iPhone.xcodeproj" \
-scheme Unity-iPhone \
-configuration Release \
-sdk iphoneos \
-derivedDataPath "${{ github.workspace }}/ios-derived" \
CODE_SIGNING_ALLOWED=NO \
build

- uses: actions/upload-artifact@v4
with:
name: build-size-iOS
path: ios-derived/Build/Products/Release-iphoneos/*.app
if-no-files-found: error

- uses: actions/upload-artifact@v4
if: always()
with:
name: build-size-ios-log
path: ios-unity-build.log

# macOS build size: Unity batch mode produces the .app directly.
build-size-macos:
needs: paths-changed
if: |
github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.fork == false
&& needs.paths-changed.outputs.audience == 'true'
name: Build size / macOS
runs-on: ["self-hosted", "macOS", "ARM64"]
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
lfs: true

- name: Cache Unity Library (macOS size)
uses: actions/cache@v4
with:
path: examples/audience/Library
key: Library-size-macOS-2021.3.45f2-${{ hashFiles('examples/audience/Assets/**', 'examples/audience/Packages/**', 'examples/audience/ProjectSettings/**', 'src/Packages/Audience/**') }}
restore-keys: Library-size-macOS-2021.3.45f2-

- name: Install Unity
env:
UNITY_VERSION: 2021.3.45f2
UNITY_CHANGESET: 88f88f591b2e
BACKEND: Mono2x
run: .github/scripts/audience/install-unity-macos.sh

- name: Build macOS app
run: |
"$UNITY_PATH" \
-projectPath "${{ github.workspace }}/examples/audience" \
-executeMethod Immutable.Audience.Samples.SampleApp.Editor.MacBuilder.Build \
-buildTarget StandaloneOSX \
-logFile "${{ github.workspace }}/macos-build.log" \
-quit -batchmode

- uses: actions/upload-artifact@v4
with:
name: build-size-macOS
path: examples/audience/Builds/macOS/AudienceSample.app
if-no-files-found: error

- uses: actions/upload-artifact@v4
if: always()
with:
name: build-size-macos-log
path: macos-build.log

# Collects Android + Windows (from mobile-build) and iOS + macOS (from dedicated jobs),
# measures sizes, posts a table comment on the PR, and fails if any platform exceeds budget.
build-size-check:
needs: [mobile-build, build-size-ios, build-size-macos]
if: github.event_name == 'pull_request'
name: Build size / check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: mobile-build-Android-2021.3.45f2
path: artifacts/Android

- uses: actions/download-artifact@v4
with:
name: mobile-build-StandaloneWindows64-2021.3.45f2
path: artifacts/Windows

- uses: actions/download-artifact@v4
with:
name: build-size-iOS
path: artifacts/iOS

- uses: actions/download-artifact@v4
with:
name: build-size-macOS
path: artifacts/macOS

- uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');

const budget = JSON.parse(fs.readFileSync('.github/audience-build-budget.json', 'utf8'));

function dirSize(dir) {
let total = 0;
const walk = d => {
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
const full = path.join(d, entry.name);
if (entry.isDirectory()) walk(full);
else total += fs.statSync(full).size;
}
};
walk(dir);
return total;
}

function apkSize(dir) {
const apk = fs.readdirSync(dir).find(f => f.endsWith('.apk'));
return apk ? fs.statSync(path.join(dir, apk)).size : 0;
}

const measured = {
Android: apkSize('artifacts/Android'),
Windows: dirSize('artifacts/Windows'),
iOS: dirSize('artifacts/iOS'),
macOS: dirSize('artifacts/macOS'),
};

let anyFail = false;
const mb = n => (n / 1048576).toFixed(2);
const rows = Object.entries(measured).map(([plat, bytes]) => {
const { baselineBytes, maxBytes } = budget.platforms[plat];
const delta = bytes - baselineBytes;
const sign = delta >= 0 ? '+' : '';
const limited = maxBytes > 0;
const pass = !limited || bytes <= maxBytes;
if (!pass) anyFail = true;
const status = !limited ? '—' : pass ? '✅' : '❌';
return `| ${plat} | ${mb(bytes)} MB | ${sign}${mb(delta)} MB | ${status} |`;
});

const limitsSet = Object.values(budget.platforms).some(p => p.maxBytes > 0);
const footer = limitsSet
? 'Fails if any platform exceeds its absolute size limit.'
: '_No size limits set yet. Run workflow\\_dispatch on main, measure the output, then set maxBytes in `.github/audience-build-budget.json`._';

const body = [
'## Audience SDK — Build Size',
'',
'| Platform | Build Size | vs Baseline | |',
'|---|---|---|---|',
...rows,
'',
footer,
].join('\n');

const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Audience SDK — Build Size')
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: existing.id, body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number, body,
});
}

if (anyFail) core.setFailed('One or more platforms exceeded the build size budget.');

# Required check. Passes immediately when no Audience paths changed;
# fails if playmode or mobile-build tests failed or were cancelled.
# fails if playmode, mobile-build, or build-size-check failed or were cancelled.
ci-gate:
needs: [playmode, mobile-build]
needs: [playmode, mobile-build, build-size-check]
if: always() && github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Check results
run: |
PLAYMODE="${{ needs.playmode.result }}"
MOBILE="${{ needs.mobile-build.result }}"
BSIZE="${{ needs.build-size-check.result }}"
if [[ "$PLAYMODE" == "failure" || "$PLAYMODE" == "cancelled" ]]; then
echo "::error::playmode tests $PLAYMODE" && exit 1
fi
if [[ "$MOBILE" == "failure" || "$MOBILE" == "cancelled" ]]; then
echo "::error::mobile-build $MOBILE" && exit 1
fi
echo "Gate passed (playmode=$PLAYMODE, mobile-build=$MOBILE)"
if [[ "$BSIZE" == "failure" ]]; then
echo "::error::build-size-check failed" && exit 1
fi
echo "Gate passed (playmode=$PLAYMODE, mobile-build=$MOBILE, build-size-check=$BSIZE)"
60 changes: 60 additions & 0 deletions examples/audience/Assets/Editor/MacBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#nullable enable

using System;
using UnityEditor;
using UnityEngine;

namespace Immutable.Audience.Samples.SampleApp.Editor
{
// Invoked by CI via:
// Unity -batchmode -buildTarget StandaloneOSX \
// -executeMethod Immutable.Audience.Samples.SampleApp.Editor.MacBuilder.Build \
// -quit
//
// Optional CLI arg:
// --buildPath <path> Output path for the .app (default: Builds/macOS/AudienceSample.app)
internal static class MacBuilder
{
private const string DefaultBuildPath = "Builds/macOS/AudienceSample.app";

public static void Build()
{
string buildPath = GetArgValue("--buildPath") ?? DefaultBuildPath;

var options = new BuildPlayerOptions
{
scenes = new[] { "Assets/SampleApp/Scenes/SampleApp.unity" },
locationPathName = buildPath,
target = BuildTarget.StandaloneOSX,
targetGroup = BuildTargetGroup.Standalone,
options = BuildOptions.None,
};

Debug.Log($"[MacBuilder] Building to: {buildPath}");

var report = BuildPipeline.BuildPlayer(options);
var summary = report.summary;

if (summary.result == UnityEditor.Build.Reporting.BuildResult.Succeeded)
{
Debug.Log($"[MacBuilder] Build succeeded ({summary.totalSize / 1024 / 1024} MB).");
}
else
{
Debug.LogError($"[MacBuilder] Build failed: {summary.totalErrors} error(s).");
EditorApplication.Exit(1);
}
}

private static string? GetArgValue(string flag)
{
var args = Environment.GetCommandLineArgs();
for (int i = 0; i < args.Length - 1; i++)
{
if (args[i] == flag)
return args[i + 1];
}
return null;
}
}
}
11 changes: 11 additions & 0 deletions examples/audience/Assets/Editor/MacBuilder.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading