Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0271e6c
unity-cli@v1.5.4
StephenHodgson Nov 5, 2025
fa2baa3
always clean space on linux
StephenHodgson Nov 6, 2025
339a5b7
add stack trace to the message
StephenHodgson Nov 6, 2025
b422a69
bump action versions
StephenHodgson Nov 6, 2025
e8d0363
bump action versions
StephenHodgson Nov 6, 2025
9c7b7cc
try to init dummy fmod audio drivers to reduce error logs
StephenHodgson Nov 6, 2025
0266d3e
init ALSA
StephenHodgson Nov 6, 2025
561392b
append summary
StephenHodgson Nov 6, 2025
031348b
reverts
StephenHodgson Nov 7, 2025
6acc591
add Completed with errors edge case
StephenHodgson Nov 9, 2025
df536b5
sure y not
StephenHodgson Nov 16, 2025
eece0e0
bump deps
StephenHodgson Nov 16, 2025
07a6c97
update android sdk search
StephenHodgson Nov 16, 2025
bd0d637
don't remove android for 2017 && 2018
StephenHodgson Nov 16, 2025
0f4f86e
update build options
StephenHodgson Nov 16, 2025
a3003b3
remove logs
StephenHodgson Nov 16, 2025
9f33b07
fix linux FMOD errors
StephenHodgson Nov 16, 2025
969a683
only annotate if script is in root of project path
StephenHodgson Nov 16, 2025
d039f15
add pulseaudio to install
StephenHodgson Nov 16, 2025
3d79e1f
only build linux
StephenHodgson Nov 16, 2025
8fd2c9a
Merge branch 'development' into fix/fmod-errors
StephenHodgson Nov 16, 2025
78caa36
Revert "only build linux"
StephenHodgson Nov 17, 2025
3a2e042
fix linux FMOD errors (#41)
StephenHodgson Nov 17, 2025
80fc045
remove comment
StephenHodgson Nov 17, 2025
7493975
add ignored error lines for editor
StephenHodgson Nov 17, 2025
e85e776
add additional log
StephenHodgson Nov 17, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 24.x
registry-url: "https://registry.npmjs.org"
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/unity-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ jobs:
RUN_BUILD: '' # Set to true if the build pipeline package can be installed and used
steps:
- name: Free Disk Space
if: ${{ matrix.os == 'ubuntu-latest' && matrix.unity-version == '6000.2' }}
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: endersonmenezes/free-disk-space@713d134e243b926eba4a5cce0cf608bfd1efb89a # v2.1.1
with:
remove_android: true
remove_dotnet: false
remove_tool_cache: false
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 24.x
- name: Setup unity-cli
Expand Down Expand Up @@ -140,9 +140,10 @@ jobs:
unity-cli return-license --license personal
- name: Upload Logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ${{ github.run_id }}.${{ github.run_attempt }} ${{ matrix.os }} ${{ matrix.unity-version }} ${{ matrix.build-target }} logs
retention-days: 1
if-no-files-found: ignore
path: |
${{ github.workspace }}/**/*.log
4 changes: 2 additions & 2 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rage-against-the-pixel/unity-cli",
"version": "1.5.3",
"version": "1.5.4",
"description": "A command line utility for the Unity Game Engine.",
"author": "RageAgainstThePixel",
"license": "MIT",
Expand Down Expand Up @@ -68,4 +68,4 @@
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
}
}
}
86 changes: 86 additions & 0 deletions src/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,74 @@ export class Logger {
this.log(LogLevel.ERROR, message, optionalParams);
}

/**
* Annotates a file and line number in CI environments that support it.
* @param logLevel The level of the log.
* @param message The message to annotate.
* @param file The file to annotate.
* @param line The line number to annotate.
* @param endLine The end line number to annotate.
* @param column The column number to annotate.
* @param endColumn The end column number to annotate.
* @param title The title of the annotation.
*/
public annotate(logLevel: LogLevel, message: string, file?: string, line?: number, endLine?: number, column?: number, endColumn?: number, title?: string): void {
let annotation = '';

switch (this._ci) {
case 'GITHUB_ACTIONS': {
var level: string;
switch (logLevel) {
case LogLevel.CI:
case LogLevel.INFO:
case LogLevel.DEBUG: {
level = 'notice';
break;
}
case LogLevel.WARN: {
level = 'warning';
break;
}
case LogLevel.ERROR: {
level = 'error';
break;
}
}

let parts: string[] = [];

if (file !== undefined && file.length > 0) {
parts.push(`file=${file}`);
}

if (line !== undefined && line > 0) {
parts.push(`line=${line}`);
}

if (endLine !== undefined && endLine > 0) {
parts.push(`endLine=${endLine}`);
}

if (column !== undefined && column > 0) {
parts.push(`col=${column}`);
}

if (endColumn !== undefined && endColumn > 0) {
parts.push(`endColumn=${endColumn}`);
}

if (title !== undefined && title.length > 0) {
parts.push(`title=${title}`);
}

annotation = `::${level} ${parts.join(',')}::${message}`;
break;
}
}

process.stdout.write(`${annotation}\n`);
}

private shouldLog(level: LogLevel): boolean {
if (level === LogLevel.CI) { return true; }
const levelOrder = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR];
Expand Down Expand Up @@ -187,4 +255,22 @@ export class Logger {
}
}
}


public CI_appendWorkflowSummary(telemetry: any[]) {
switch (this._ci) {
case 'GITHUB_ACTIONS': {
const githubSummary = process.env.GITHUB_STEP_SUMMARY;

if (githubSummary) {
let table = `| Key | Value |\n| --- | ----- |\n`;
telemetry.forEach(item => {
table += `| ${item.key} | ${item.value} |\n`;
});

fs.writeFileSync(githubSummary, table, { encoding: 'utf8' });
}
}
}
}
}
40 changes: 30 additions & 10 deletions src/unity-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,18 +269,38 @@
throw new Error(`Cannot execute Unity ${this.version.toString()} on Apple Silicon Macs.`);
}

if (process.platform === 'linux' &&
!command.args.includes('-nographics')
) {
if (process.platform === 'linux') {
// On Linux, force Unity to run under Xvfb and provide a dummy audio driver
// to prevent FMOD from failing to initialize the output device when no
// actual audio device is present (common in CI/container environments).
const linuxEnv = {
...process.env,
DISPLAY: ':99',
UNITY_THISISABUILDMACHINE: '1',
// Tell various audio systems to use a dummy/out-of-process driver
SDL_AUDIODRIVER: process.env.SDL_AUDIODRIVER || 'dummy',
AUDIODRIVER: process.env.AUDIODRIVER || 'dummy',
AUDIODEV: process.env.AUDIODEV || 'null',
// For PulseAudio: point to an invalid socket to avoid connecting
PULSE_SERVER: process.env.PULSE_SERVER || '/tmp/invalid-pulse-socket'
};

var linuxCommand = '';
var linuxArgs = [];

if (!command.args.includes('-nographics')) {
linuxCommand = 'xvfb-run';
linuxArgs = [this.editorPath, ...command.args];
} else {
linuxCommand = this.editorPath;
linuxArgs = command.args;
}

unityProcess = spawn(
'xvfb-run',
[this.editorPath, ...command.args], {
linuxCommand,
linuxArgs, {
stdio: ['ignore', 'ignore', 'ignore'],
env: {
...process.env,
DISPLAY: ':99',
UNITY_THISISABUILDMACHINE: '1'
}
env: linuxEnv
});
} else if (process.arch === 'arm64' &&
process.platform === 'darwin' &&
Expand Down
4 changes: 2 additions & 2 deletions src/unity-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ set -e
wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | sudo tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null
sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list'
sudo apt-get update --allow-releaseinfo-change
sudo apt-get install -y --no-install-recommends --only-upgrade unityhub${version ? '=' + version : ''}`]);
sudo apt-get install -y --no-install-recommends --only-upgrade unityhub${version ? '=' + version : ''} libasound2 alsa-utils`]);
} else {
throw new Error(`Unsupported platform: ${process.platform}`);
}
Expand Down Expand Up @@ -439,7 +439,7 @@ wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | tee /usr/
echo "deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list
echo "deb https://archive.ubuntu.com/ubuntu jammy main universe" | tee /etc/apt/sources.list.d/jammy.list
apt-get update
apt-get install -y --no-install-recommends unityhub${version ? '=' + version : ''} ffmpeg libgtk2.0-0 libglu1-mesa libgconf-2-4 libncurses5
apt-get install -y --no-install-recommends unityhub${version ? '=' + version : ''} ffmpeg libgtk2.0-0 libglu1-mesa libgconf-2-4 libncurses5 libasound2 alsa-utils
apt-get clean
sed -i 's/^\\(.*DISPLAY=:.*XAUTHORITY=.*\\)\\( "\\$@" \\)2>&1$/\\1\\2/' /usr/bin/xvfb-run
printf '#!/bin/bash\nxvfb-run --auto-servernum /opt/unityhub/unityhub "$@" 2>/dev/null' | tee /usr/bin/unity-hub >/dev/null
Expand Down
47 changes: 37 additions & 10 deletions src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ export interface LogTailResult {
tailPromise: Promise<void>;
/** Function to signal that log tailing should end */
stopLogTail: () => void;
/** Collected telemetry objects parsed from lines beginning with '##utp:' */
telemetry: any[];
}

/**
Expand All @@ -345,18 +347,13 @@ export function TailLogFile(logPath: string): LogTailResult {
let logEnded = false;
let lastSize = 0;
const logPollingInterval = 250;
const telemetry: any[] = [];

async function readNewLogContent(): Promise<void> {
try {
if (!fs.existsSync(logPath)) {
return;
}

if (!fs.existsSync(logPath)) { return; }
const stats = await fs.promises.stat(logPath);

if (stats.size < lastSize) {
lastSize = 0;
}
if (stats.size < lastSize) { lastSize = 0; }

if (stats.size > lastSize) {
const bytesToRead = stats.size - lastSize;
Expand All @@ -375,12 +372,41 @@ export function TailLogFile(logPath: string): LogTailResult {
if (bytesToRead > 0) {
const chunk = buffer.toString('utf8');

// Parse telemetry lines in this chunk (lines starting with '##utp:')
try {
process.stdout.write(chunk);
const lines = chunk.split(/\r?\n/);
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) { continue; }
if (line.startsWith('##utp:')) {
const jsonPart = line.substring('##utp:'.length).trim();
try {
const utp = JSON.parse(jsonPart);
telemetry.push(utp);

// annotate the log with the telemetry event
// ##utp:{"type":"Compiler","version":2,"phase":"Immediate","time":1762378495689,"processId":2256,"severity":"Error","message":"Assets\\_BurnerSphere\\Content\\Common Assets\\Lighting\\older\\ReflectionProbeBaker1.cs(75,13): error CS0103: The name 'AssetDatabase' does not exist in the current context","stacktrace":"","line":75,"file":"Assets\\_BurnerSphere\\Content\\Common Assets\\Lighting\\older\\ReflectionProbeBaker1.cs"}
if (utp.severity && utp.severity.toLowerCase() === 'error') {
const file = utp.file ? utp.file.replace(/\\/g, '/') : undefined;
const lineNum = utp.line ? utp.line : undefined;
const message = utp.message;
const stacktrace = utp.stacktrace ? `${utp.stacktrace}` : undefined;
if (!message.startsWith(`\n::error::\u001B[31m`)) { // indicates a duplicate annotation
Logger.instance.annotate(LogLevel.ERROR, stacktrace == undefined ? message : `${message}\n${stacktrace}`, file, lineNum);
}
}
} catch (error) {
logger.warn(`Failed to parse telemetry JSON: ${error} -- raw: ${jsonPart}`);
}
} else {
process.stdout.write(`${line}\n`);
}
}
} catch (error: any) {
if (error.code !== 'EPIPE') {
throw error;
}
logger.warn(`Error while parsing telemetry from log chunk: ${error}`);
}
}
}
Expand All @@ -402,6 +428,7 @@ export function TailLogFile(logPath: string): LogTailResult {
await readNewLogContent();

try {
// write a final newline to separate log output
process.stdout.write('\n');
} catch (error: any) {
if (error.code !== 'EPIPE') {
Expand All @@ -420,7 +447,7 @@ export function TailLogFile(logPath: string): LogTailResult {
logEnded = true;
}

return { tailPromise, stopLogTail };
return { tailPromise, stopLogTail, telemetry };
}

/**
Expand Down
11 changes: 6 additions & 5 deletions tests/mocks/electron-asar.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Minimal mock for @electron/asar used in tests
module.exports = {
extractFile: (asarPath, file) => {
if (file === 'package.json') {
return Buffer.from(JSON.stringify({ version: '1.0.0' }));
}
return Buffer.from('');
extractFile: (asarPath, file) => {
if (file === 'package.json') {
return Buffer.from(JSON.stringify({ version: '1.0.0' }));
}
return Buffer.from('');
},
uncacheAll: () => { },
};
Loading