From 04f22742721986f8cfab4d72493bc7bdf2abb7b1 Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Sat, 30 May 2026 18:18:43 +0300 Subject: [PATCH] New: Add Android build support on macOS Enable Android SDK/NDK discovery on macOS, use the darwin-x86_64 NDK toolchain, and support the modern NDK include layout used by NDK r27. --- .../Platform/Android/AndroidPlatformTools.cpp | 19 ++++++++++--------- Source/Engine/Core/Config/GameSettings.cs | 11 ++++++----- .../Platform/Android/AndroidPlatform.cpp | 2 +- Source/Engine/Platform/Mac/MacPlatform.cpp | 7 +++++++ .../Platforms/Android/AndroidNdk.cs | 1 + .../Platforms/Android/AndroidSdk.cs | 4 ++++ .../Platforms/Android/AndroidToolchain.cs | 15 +++++++++++++-- 7 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp index 1b6e7a616..be46f1c26 100644 --- a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp @@ -330,18 +330,16 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) GameCooker::PackageFiles(); // Validate environment variables - Dictionary envVars; - Platform::GetEnvironmentVariables(envVars); String javaHome; - if (!envVars.TryGet(TEXT("JAVA_HOME"), javaHome) || !FileSystem::DirectoryExists(javaHome)) + if (Platform::GetEnvironmentVariable(TEXT("JAVA_HOME"), javaHome) || !FileSystem::DirectoryExists(javaHome)) { LOG(Error, "Missing or invalid JAVA_HOME env variable. {0}", javaHome); return true; } String androidSdk; - if (!envVars.TryGet(TEXT("ANDROID_HOME"), androidSdk) || !FileSystem::DirectoryExists(androidSdk)) + if (Platform::GetEnvironmentVariable(TEXT("ANDROID_HOME"), androidSdk) || !FileSystem::DirectoryExists(androidSdk)) { - if (!envVars.TryGet(TEXT("ANDROID_SDK"), androidSdk) || !FileSystem::DirectoryExists(androidSdk)) + if (Platform::GetEnvironmentVariable(TEXT("ANDROID_SDK"), androidSdk) || !FileSystem::DirectoryExists(androidSdk)) { LOG(Error, "Missing or invalid ANDROID_HOME env variable. {0}", androidSdk); return true; @@ -355,10 +353,11 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) #else const Char* gradlew = TEXT("gradlew"); #endif -#if PLATFORM_LINUX +#if PLATFORM_LINUX || PLATFORM_MAC { CreateProcessSettings procSettings; - procSettings.FileName = String::Format(TEXT("chmod +x \"{0}/gradlew\""), data.OriginalOutputPath); + procSettings.FileName = TEXT("/bin/chmod"); + procSettings.Arguments = String::Format(TEXT("+x \"{0}\""), data.OriginalOutputPath / gradlew); procSettings.WorkingDirectory = data.OriginalOutputPath; procSettings.HiddenWindow = true; Platform::CreateProcess(procSettings); @@ -371,7 +370,8 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) // .aab { CreateProcessSettings procSettings; - procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT(":app:bundle") : TEXT(":app:bundleDebug")); + procSettings.FileName = data.OriginalOutputPath / gradlew; + procSettings.Arguments = distributionPackage ? TEXT("--console=plain :app:bundle") : TEXT("--console=plain :app:bundleDebug"); procSettings.WorkingDirectory = data.OriginalOutputPath; const int32 result = Platform::CreateProcess(procSettings); if (result != 0) @@ -394,7 +394,8 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) // .apk { CreateProcessSettings procSettings; - procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT("assemble") : TEXT("assembleDebug")); + procSettings.FileName = data.OriginalOutputPath / gradlew; + procSettings.Arguments = distributionPackage ? TEXT("--console=plain assemble") : TEXT("--console=plain assembleDebug"); procSettings.WorkingDirectory = data.OriginalOutputPath; const int32 result = Platform::CreateProcess(procSettings); if (result != 0) diff --git a/Source/Engine/Core/Config/GameSettings.cs b/Source/Engine/Core/Config/GameSettings.cs index 4f66261be..5fa96f98c 100644 --- a/Source/Engine/Core/Config/GameSettings.cs +++ b/Source/Engine/Core/Config/GameSettings.cs @@ -14,6 +14,7 @@ namespace FlaxEditor.Content.Settings internal const string PS5PlatformSettingsTypename = "FlaxEditor.Content.Settings.PS5PlatformSettings"; internal const string XboxOnePlatformSettingsTypename = "FlaxEditor.Content.Settings.XboxOnePlatformSettings"; internal const string XboxScarlettPlatformSettingsTypename = "FlaxEditor.Content.Settings.XboxScarlettPlatformSettings"; + internal const string AndroidPlatformSettingsTypename = "FlaxEditor.Content.Settings.AndroidPlatformSettings"; internal const string SwitchPlatformSettingsTypename = "FlaxEditor.Content.Settings.SwitchPlatformSettings"; #if FLAX_EDITOR internal static string[] OptionalPlatformSettings = @@ -172,9 +173,9 @@ namespace FlaxEditor.Content.Settings #if FLAX_EDITOR || PLATFORM_ANDROID /// - /// Reference to asset. Used to apply configuration on Android platform. + /// Reference to Android Platform Settings asset. Used to apply configuration on Android platform. /// - [EditorOrder(2060), EditorDisplay("Platform Settings", "Android"), AssetReference(typeof(AndroidPlatformSettings), true), Tooltip("Reference to Android Platform Settings asset")] + [EditorOrder(2060), EditorDisplay("Platform Settings", "Android"), AssetReference(AndroidPlatformSettingsTypename, true), Tooltip("Reference to Android Platform Settings asset")] public JsonAsset AndroidPlatform; #endif @@ -334,8 +335,8 @@ namespace FlaxEditor.Content.Settings return Load(gameSettings.XboxScarlettPlatform, XboxScarlettPlatformSettingsTypename) as T; #endif #if FLAX_EDITOR || PLATFORM_ANDROID - if (type == typeof(AndroidPlatformSettings)) - return Load(gameSettings.AndroidPlatform) as T; + if (type.FullName == AndroidPlatformSettingsTypename) + return Load(gameSettings.AndroidPlatform, AndroidPlatformSettingsTypename) as T; #endif #if FLAX_EDITOR || PLATFORM_SWITCH if (type.FullName == SwitchPlatformSettingsTypename) @@ -436,7 +437,7 @@ namespace FlaxEditor.Content.Settings return gameSettings.XboxScarlettPlatform; #endif #if FLAX_EDITOR || PLATFORM_ANDROID - if (type == typeof(AndroidPlatformSettings)) + if (type.FullName == AndroidPlatformSettingsTypename) return gameSettings.AndroidPlatform; #endif #if FLAX_EDITOR || PLATFORM_SWITCH diff --git a/Source/Engine/Platform/Android/AndroidPlatform.cpp b/Source/Engine/Platform/Android/AndroidPlatform.cpp index 0e2f9e686..08aafe358 100644 --- a/Source/Engine/Platform/Android/AndroidPlatform.cpp +++ b/Source/Engine/Platform/Android/AndroidPlatform.cpp @@ -963,7 +963,7 @@ void AndroidPlatform::Tick() // Pool app events int events; android_poll_source* source; - while (ALooper_pollAll(0, nullptr, &events, reinterpret_cast(&source)) >= 0) + while (ALooper_pollOnce(0, nullptr, &events, reinterpret_cast(&source)) >= 0) { // Process event if (source != nullptr) diff --git a/Source/Engine/Platform/Mac/MacPlatform.cpp b/Source/Engine/Platform/Mac/MacPlatform.cpp index 4e268d3f9..2f8d64aed 100644 --- a/Source/Engine/Platform/Mac/MacPlatform.cpp +++ b/Source/Engine/Platform/Mac/MacPlatform.cpp @@ -471,6 +471,13 @@ int32 MacPlatform::CreateProcess(CreateProcessSettings& settings) task.arguments = AppleUtils::ParseArguments(AppleUtils::ToNSString(settings.Arguments)); if (settings.WorkingDirectory.HasChars()) task.currentDirectoryPath = AppleUtils::ToNSString(settings.WorkingDirectory); + [task setStandardInput:[NSFileHandle fileHandleWithNullDevice]]; + if (!captureStdOut) + { + NSFileHandle* nullDevice = [NSFileHandle fileHandleWithNullDevice]; + [task setStandardOutput:nullDevice]; + [task setStandardError:nullDevice]; + } int32 returnCode = 0; if (settings.WaitForEnd) diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs index 00838a73d..a67e6dab7 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs @@ -22,6 +22,7 @@ namespace Flax.Build.Platforms { TargetPlatform.Windows, TargetPlatform.Linux, + TargetPlatform.Mac, }; /// diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidSdk.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidSdk.cs index d3b169117..35eb4de94 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidSdk.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidSdk.cs @@ -22,6 +22,7 @@ namespace Flax.Build.Platforms { TargetPlatform.Windows, TargetPlatform.Linux, + TargetPlatform.Mac, }; /// @@ -106,6 +107,9 @@ namespace Flax.Build.Platforms case TargetPlatform.Linux: hostName = "linux-x86_64"; break; + case TargetPlatform.Mac: + hostName = "darwin-x86_64"; + break; default: throw new InvalidPlatformException(Platform.BuildPlatform.Target); } return hostName; diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs index 34b77ae5a..3ce690df4 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Flax.Build.Graph; using Flax.Build.NativeCpp; @@ -37,10 +38,20 @@ namespace Flax.Build.Platforms : base(platform, architecture, toolchainRoot, null, string.Empty) { var toolchain = ToolsetRoot.Replace('\\', '/'); - SystemIncludePaths.Add(Path.Combine(toolchain, "sources/usr/include/c++/v1").Replace('\\', '/')); + var cxxIncludePath = Path.Combine(toolchain, "sysroot/usr/include/c++/v1"); + if (!Directory.Exists(cxxIncludePath)) + cxxIncludePath = Path.Combine(toolchain, "sources/usr/include/c++/v1"); + SystemIncludePaths.Add(cxxIncludePath.Replace('\\', '/')); SystemIncludePaths.Add(Path.Combine(toolchain, "sysroot/usr/include").Replace('\\', '/')); SystemIncludePaths.Add(Path.Combine(toolchain, "sysroot/usr/local/include").Replace('\\', '/')); - SystemIncludePaths.Add(Path.Combine(toolchain, "lib64/clang/9.0.8/include").Replace('\\', '/')); + var clangIncludeRoot = Path.Combine(toolchain, "lib/clang"); + if (!Directory.Exists(clangIncludeRoot)) + clangIncludeRoot = Path.Combine(toolchain, "lib64/clang"); + var clangIncludePath = Directory.Exists(clangIncludeRoot) + ? Directory.GetDirectories(clangIncludeRoot).OrderBy(x => x).LastOrDefault() + : null; + if (clangIncludePath != null) + SystemIncludePaths.Add(Path.Combine(clangIncludePath, "include").Replace('\\', '/')); } ///