Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a5389d0
unity-cli@v1.3.4
StephenHodgson Oct 18, 2025
4108daf
move it outside
StephenHodgson Oct 18, 2025
1070a50
free disk space for linux
StephenHodgson Oct 18, 2025
f00ec6a
test setting target android sdk
StephenHodgson Oct 18, 2025
14fdfce
test android on windows runner
StephenHodgson Oct 18, 2025
b54be1b
run free disk space first
StephenHodgson Oct 18, 2025
43aa196
update build options
StephenHodgson Oct 18, 2025
4a30a4a
fix AndroidTargetSdkVersion set
StephenHodgson Oct 18, 2025
115f280
run unity setup again after changing the target android sdk version
StephenHodgson Oct 18, 2025
a499dfa
fix setup args
StephenHodgson Oct 18, 2025
a24bcfe
Update unity-cli setup command in workflow
StephenHodgson Oct 18, 2025
1567daa
print project version text content
StephenHodgson Oct 18, 2025
c9d5896
comment
StephenHodgson Oct 18, 2025
36cc2b5
fix project version text file parsing
StephenHodgson Oct 18, 2025
f364589
fix artifact logs
StephenHodgson Oct 18, 2025
1f11a4f
fix module validation
StephenHodgson Oct 18, 2025
463203a
refactor android sdk installation
StephenHodgson Oct 18, 2025
644eb53
fix semver check
StephenHodgson Oct 18, 2025
4afe82b
add gt/ge/lt/le functions to unity version
StephenHodgson Oct 18, 2025
98032df
base compare without build metadata
StephenHodgson Oct 18, 2025
94c0b0f
update logging
StephenHodgson Oct 18, 2025
b027341
update sdkmanager path
StephenHodgson Oct 19, 2025
e5d0ea1
fix sdkmanager for 2019 & 2020
StephenHodgson Oct 19, 2025
2966894
bump version to 1.4.0
StephenHodgson Oct 19, 2025
34df581
fix android sdk range
StephenHodgson Oct 19, 2025
a423922
add transient network retry when fetching version release info
StephenHodgson Oct 19, 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
14 changes: 14 additions & 0 deletions .github/workflows/build-options.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"os": "windows-latest",
"build-target": "StandaloneWindows64"
},
{
"os": "windows-latest",
"build-target": "Android"
},
{
"os": "macos-latest",
"build-target": "StandaloneOSX"
Expand All @@ -53,6 +57,11 @@
"os": "macos-latest",
"unity-version": "4.7.2"
},
{
"os": "windows-latest",
"build-target": "Android",
"unity-version": "4.7.2"
},
{
"os": "ubuntu-latest",
"unity-version": "5.6.7f1 (e80cc3114ac1)"
Expand All @@ -61,6 +70,11 @@
"os": "macos-latest",
"build-target": "iOS",
"unity-version": "5.6.7f1 (e80cc3114ac1)"
},
{
"os": "windows-latest",
"build-target": "Android",
"unity-version": "5.6.7f1 (e80cc3114ac1)"
}
]
}
17 changes: 16 additions & 1 deletion .github/workflows/unity-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ jobs:
UNITY_PROJECT_PATH: '' # Set from create-project step
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' }}
uses: endersonmenezes/free-disk-space@v2
with:
remove_android: true
remove_dotnet: false
remove_tool_cache: false
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
Expand Down Expand Up @@ -96,6 +103,14 @@ jobs:
npm install -g openupm-cli
cd "${UNITY_PROJECT_PATH}"
openupm add com.utilities.buildpipeline
- name: Update Android Target Sdk Version
if: ${{ matrix.build-target == 'Android' }}
shell: bash
run: |
# update AndroidTargetSdkVersion to 32 in ProjectSettings/ProjectSettings.asset
sed -i 's/AndroidTargetSdkVersion: [0-9]*/AndroidTargetSdkVersion: 32/' "${UNITY_PROJECT_PATH}/ProjectSettings/ProjectSettings.asset"
# ensure android dependencies are installed
unity-cli setup-unity -p "${UNITY_PROJECT_PATH}" -m android
- name: Build Project
if: ${{ env.RUN_BUILD == 'true' }}
timeout-minutes: 60
Expand All @@ -120,7 +135,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ github.run_id }}.${{ github.run_attempt }} ${{ matrix.unity-version }} ${{ matrix.build-target }} logs
name: ${{ github.run_id }}.${{ github.run_attempt }} ${{ matrix.os }} ${{ matrix.unity-version }} ${{ matrix.build-target }} logs
retention-days: 1
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.

2 changes: 1 addition & 1 deletion 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.3.3",
"version": "1.4.0",
"description": "A command line utility for the Unity Game Engine.",
"author": "RageAgainstThePixel",
"license": "MIT",
Expand Down
149 changes: 108 additions & 41 deletions src/android-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ import {
ReadFileContents,
ResolveGlobToPath
} from './utilities';
import { satisfies } from 'semver';

const logger = Logger.instance;

/**
* Checks if the required Android SDK is installed for the given Unity Editor and Project.
* @param editorPath The path to the Unity Editor executable.
* @param editor The UnityEditor instance.
* @param projectPath The path to the Unity project.
* @returns A promise that resolves when the check is complete.
*/
export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: string): Promise<void> {
logger.ci(`Checking Android SDK installation for:\n > Editor: ${editorPath}\n > Project: ${projectPath}`);
export async function CheckAndroidSdkInstalled(editor: UnityEditor, projectPath: string): Promise<void> {
logger.ci(`Checking Android SDK installation for:\n > Editor: ${editor.editorRootPath}\n > Project: ${projectPath}`);
let sdkPath = undefined;
await createRepositoryCfg();
const rootEditorPath = UnityEditor.GetEditorRootPath(editorPath);
const projectSettingsPath = path.join(projectPath, 'ProjectSettings/ProjectSettings.asset');
const projectSettingsContent = await ReadFileContents(projectSettingsPath);
const matchResult = projectSettingsContent.match(/(?<=AndroidTargetSdkVersion: )\d+/);
Expand All @@ -31,23 +31,23 @@ export async function CheckAndroidSdkInstalled(editorPath: string, projectPath:

if (androidTargetSdk === undefined || androidTargetSdk === 0) { return; }

sdkPath = await getAndroidSdkPath(rootEditorPath, androidTargetSdk);
sdkPath = await getAndroidSdkPath(editor, androidTargetSdk);

if (sdkPath) {
logger.ci(`Target Android SDK android-${androidTargetSdk} Installed in:\n > "${sdkPath}"`);
return;
}

logger.info(`Installing Android Target SDK:\n > android-${androidTargetSdk}`);
const sdkManagerPath = await getSdkManager(rootEditorPath);
const javaSdk = await getJDKPath(rootEditorPath);
const sdkManagerPath = await getSdkManager(editor);
const javaSdk = await getJDKPath(editor);
await execSdkManager(sdkManagerPath, javaSdk, ['--licenses']);
await execSdkManager(sdkManagerPath, javaSdk, ['--update']);
await execSdkManager(sdkManagerPath, javaSdk, ['platform-tools', `platforms;android-${androidTargetSdk}`]);
sdkPath = await getAndroidSdkPath(rootEditorPath, androidTargetSdk);
sdkPath = await getAndroidSdkPath(editor, androidTargetSdk);

if (!sdkPath) {
throw new Error(`Failed to install android-${androidTargetSdk} in ${rootEditorPath}`);
throw new Error(`Failed to install android-${androidTargetSdk} in ${editor.editorRootPath}`);
}

logger.ci(`Target Android SDK Installed in:\n > "${sdkPath}"`);
Expand All @@ -61,31 +61,79 @@ async function createRepositoryCfg(): Promise<void> {
await fileHandle.close();
}

async function getJDKPath(rootEditorPath: string): Promise<string> {
const jdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'AndroidPlayer', 'OpenJDK']);
async function getJDKPath(editor: UnityEditor): Promise<string> {
let jdkPath: string | undefined = undefined;

if (!jdkPath) {
throw new Error(`Failed to resolve OpenJDK in ${rootEditorPath}`);
if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) {
logger.debug('Using JDK bundled with Unity 2019+');
jdkPath = await ResolveGlobToPath([editor.editorRootPath, '**', 'AndroidPlayer', 'OpenJDK/']);

if (!jdkPath) {
throw new Error(`Failed to resolve OpenJDK in ${editor.editorRootPath}`);
}
} else {
logger.debug('Using system JDK for Unity versions prior to 2019');
jdkPath = process.env.JAVA_HOME || process.env.JDK_HOME;

if (!jdkPath) {
throw new Error('JDK installation not found: No system JAVA_HOME or JDK_HOME defined');
}
}

await fs.promises.access(jdkPath, fs.constants.R_OK);
logger.ci(`jdkPath:\n > "${jdkPath}"`);
return jdkPath;
}

async function getSdkManager(rootEditorPath: string): Promise<string> {
async function getSdkManager(editor: UnityEditor): Promise<string> {
let globPath: string[] = [];
switch (process.platform) {
case 'darwin':
case 'linux':
globPath = [rootEditorPath, '**', 'AndroidPlayer', '**', 'sdkmanager'];
break;
case 'win32':
globPath = [rootEditorPath, '**', 'AndroidPlayer', '**', 'sdkmanager.bat'];
break;
default:
throw new Error(`Unsupported platform: ${process.platform}`);
if (editor.version.range('>=2019.0.0 <2021.0.0')) {
logger.debug('Using sdkmanager bundled with Unity 2019 and 2020');
switch (process.platform) {
case 'darwin':
case 'linux':
globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'sdkmanager'];
break;
case 'win32':
globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'sdkmanager.bat'];
break;
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
} else if (editor.version.range('>=2021.0.0')) {
logger.debug('Using cmdline-tools sdkmanager bundled with Unity 2021+');
switch (process.platform) {
case 'darwin':
case 'linux':
globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'cmdline-tools', '**', 'sdkmanager'];
break;
case 'win32':
globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'cmdline-tools', '**', 'sdkmanager.bat'];
break;
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
} else {
logger.debug('Using system sdkmanager');
const systemSdkPath = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME;

if (!systemSdkPath) {
throw new Error('Android installation not found: No system ANDROID_SDK_ROOT or ANDROID_HOME defined');
}

switch (process.platform) {
case 'darwin':
case 'linux':
globPath = [systemSdkPath, 'cmdline-tools', 'latest', 'bin', 'sdkmanager'];
break;
case 'win32':
globPath = [systemSdkPath, 'cmdline-tools', 'latest', 'bin', 'sdkmanager.bat'];
break;
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
}

const sdkmanagerPath = await ResolveGlobToPath(globPath);

if (!sdkmanagerPath) {
Expand All @@ -97,19 +145,36 @@ async function getSdkManager(rootEditorPath: string): Promise<string> {
return sdkmanagerPath;
}

async function getAndroidSdkPath(rootEditorPath: string, androidTargetSdk: number): Promise<string | undefined> {
logger.ci(`Attempting to locate Android SDK Path...\n > editorPath: ${rootEditorPath}\n > androidTargetSdk: ${androidTargetSdk}`);
async function getAndroidSdkPath(editor: UnityEditor, androidTargetSdk: number): Promise<string | undefined> {
logger.ci(`Attempting to locate Android SDK Path...\n > editorRootPath: ${editor.editorRootPath}\n > androidTargetSdk: ${androidTargetSdk}`);
let sdkPath: string;

try {
sdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]);
await fs.promises.access(sdkPath, fs.constants.R_OK);
} catch (error) {
logger.debug(`android-${androidTargetSdk} not installed`);
return undefined;
// if 2019+ test editor path, else use system android installation
if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) {
logger.debug('Using Android SDK bundled with Unity 2019+');
try {
sdkPath = await ResolveGlobToPath([editor.editorRootPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]);
} catch (error) {
logger.debug(`android-${androidTargetSdk} not installed`);
return undefined;
}
} else { // fall back to system android installation
logger.debug('Using system Android SDK for Unity versions prior to 2019');
try {
const systemSdkPath = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME;

if (!systemSdkPath) {
logger.debug('Android installation not found: No system ANDROID_SDK_ROOT or ANDROID_HOME defined');
return undefined;
}

sdkPath = await ResolveGlobToPath([systemSdkPath, 'platforms', `android-${androidTargetSdk}/`]);
} catch (error) {
logger.debug(`android-${androidTargetSdk} not installed`);
return undefined;
}
}

logger.ci(`Android sdkPath:\n > "${sdkPath}"`);
return sdkPath;
}

Expand All @@ -123,25 +188,27 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st
fs.accessSync(sdkManagerPath, fs.constants.R_OK | fs.constants.X_OK);
}

if (process.platform === 'win32' && !await isProcessElevated()) {
throw new Error('Android SDK installation requires elevated (administrator) privileges. Please rerun as Administrator.');
}

try {
exitCode = await new Promise<number>((resolve, reject) => {
exitCode = await new Promise<number>(async (resolve, reject) => {
let cmdEnv = { ...process.env };
cmdEnv.JAVA_HOME = javaPath;
cmdEnv.JDK_HOME = javaPath;
cmdEnv.SKIP_JDK_VERSION_CHECK = 'true';
let cmd = sdkManagerPath;
let cmdArgs = args;

if (process.platform === 'win32') {
if (!isProcessElevated()) {
throw new Error('Android SDK installation requires elevated (administrator) privileges. Please rerun as Administrator.');
}

cmd = 'cmd.exe';
cmdArgs = ['/c', sdkManagerPath, ...args];
}

const child = spawn(cmd, cmdArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
JAVA_HOME: process.platform === 'win32' ? `"${javaPath}"` : javaPath
}
env: cmdEnv
});
const sigintHandler = () => child.kill('SIGINT');
const sigtermHandler = () => child.kill('SIGTERM');
Expand Down
11 changes: 9 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,14 @@ program.command('setup-unity')
process.exit(1);
}

const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset, options.arch);
let unityVersion: UnityVersion;

if (options.unityVersion) {
unityVersion = new UnityVersion(options.unityVersion, options.changeset, options.arch);
} else {
unityVersion = unityProject!.version;
}

const modules: string[] = options.modules ? options.modules.split(/[ ,]+/).filter(Boolean) : [];
const buildTargets: string[] = options.buildTargets ? options.buildTargets.split(/[ ,]+/).filter(Boolean) : [];
const moduleBuildTargetMap = UnityHub.GetPlatformTargetModuleMap();
Expand Down Expand Up @@ -262,7 +269,7 @@ program.command('setup-unity')
output['UNITY_PROJECT_PATH'] = unityProject.projectPath;

if (modules.includes('android')) {
await CheckAndroidSdkInstalled(unityEditor.editorPath, unityProject.projectPath);
await CheckAndroidSdkInstalled(unityEditor, unityProject.projectPath);
}
}

Expand Down
Loading
Loading