2022-05-24 19:41:41 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
using System.Xml ;
using System.Diagnostics ;
using System.IO ;
using Microsoft.Win32 ;
using System.Xml.Linq ;
using EpicGames.Core ;
using System.Security.Cryptography ;
using UnrealBuildBase ;
2022-05-25 19:55:37 -04:00
using Microsoft.Extensions.Logging ;
2022-05-24 19:41:41 -04:00
namespace UnrealBuildTool
{
class UEDeployAndroid : UEBuildDeploy , IAndroidDeploy
{
private const string XML_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" ;
// filename of current BundleTool
private const string BUNDLETOOL_JAR = "bundletool-all-0.13.0.jar" ;
// classpath of default android build tools gradle plugin
private const string ANDROID_TOOLS_BUILD_GRADLE_VERSION = "com.android.tools.build:gradle:4.0.0" ;
// name of the only vulkan validation layer we're interested in
private const string ANDROID_VULKAN_VALIDATION_LAYER = "libVkLayer_khronos_validation.so" ;
// Minimum Android SDK that must be used for Java compiling
readonly int MinimumSDKLevel = 28 ;
// Minimum SDK version needed for App Bundles
readonly int MinimumSDKLevelForBundle = 21 ;
// Minimum SDK version needed for Gradle based on active plugins
private int MinimumSDKLevelForGradle = 19 ;
// Reserved Java keywords not allowed in package names without modification
static private string [ ] JavaReservedKeywords = new string [ ] {
"abstract" , "assert" , "boolean" , "break" , "byte" , "case" , "catch" , "char" , "class" , "const" , "continue" , "default" , "do" ,
"double" , "else" , "enum" , "extends" , "final" , "finally" , "float" , "for" , "goto" , "if" , "implements" , "import" , "instanceof" ,
"int" , "interface" , "long" , "native" , "new" , "package" , "private" , "protected" , "public" , "return" , "short" , "static" ,
"strictfp" , "super" , "switch" , "sychronized" , "this" , "throw" , "throws" , "transient" , "try" , "void" , "volatile" , "while" ,
"false" , "null" , "true"
} ;
/// <summary>
/// Internal usage for GetApiLevel
/// </summary>
private List < string > ? PossibleApiLevels = null ;
protected FileReference ? ProjectFile ;
/// <summary>
/// Determines whether we package data inside the APK. Based on and OR of "-ForcePackageData" being
/// false and bPackageDataInsideApk in /Script/AndroidRuntimeSettings.AndroidRuntimeSettings being true
/// </summary>
protected bool bPackageDataInsideApk = false ;
/// <summary>
/// Ignore AppBundle (AAB) generation setting if "-ForceAPKGeneration" specified
/// </summary>
[CommandLine("-ForceAPKGeneration", Value = "true")]
public bool ForceAPKGeneration = false ;
2022-05-25 19:55:37 -04:00
public UEDeployAndroid ( FileReference ? InProjectFile , bool InForcePackageData , ILogger InLogger )
: base ( InLogger )
2022-05-24 19:41:41 -04:00
{
ProjectFile = InProjectFile ;
// read the ini value and OR with the command line value
bool IniValue = ReadPackageDataInsideApkFromIni ( null ) ;
bPackageDataInsideApk = InForcePackageData | | IniValue = = true ;
2022-05-25 19:55:37 -04:00
CommandLine . ParseArguments ( Environment . GetCommandLineArgs ( ) , this , Logger ) ;
2022-05-24 19:41:41 -04:00
}
private UnrealPluginLanguage ? UPL = null ;
private string ActiveUPLFiles = "" ;
private string? UPLHashCode = null ;
private bool ARCorePluginEnabled = false ;
private bool FacebookPluginEnabled = false ;
private bool OculusMobilePluginEnabled = false ;
private bool GoogleVRPluginEnabled = false ;
private bool EOSSDKPluginEnabled = false ;
public void SetAndroidPluginData ( List < string > Architectures , List < string > inPluginExtraData )
{
List < string > NDKArches = new List < string > ( ) ;
foreach ( string NDKArch in Architectures )
{
if ( ! NDKArches . Contains ( NDKArch ) )
{
NDKArches . Add ( GetNDKArch ( NDKArch ) ) ;
}
}
// check if certain plugins are enabled
ARCorePluginEnabled = false ;
FacebookPluginEnabled = false ;
OculusMobilePluginEnabled = false ;
GoogleVRPluginEnabled = false ;
EOSSDKPluginEnabled = false ;
ActiveUPLFiles = "" ;
foreach ( string Plugin in inPluginExtraData )
{
ActiveUPLFiles + = Plugin + "\n" ;
// check if the Facebook plugin was enabled
if ( Plugin . Contains ( "OnlineSubsystemFacebook_UPL" ) )
{
FacebookPluginEnabled = true ;
continue ;
}
// check if the ARCore plugin was enabled
if ( Plugin . Contains ( "GoogleARCoreBase_APL" ) )
{
ARCorePluginEnabled = true ;
continue ;
}
// check if the Oculus Mobile plugin was enabled
if ( Plugin . Contains ( "OculusMobile_APL" ) )
{
OculusMobilePluginEnabled = true ;
continue ;
}
// check if the GoogleVR plugin was enabled
if ( Plugin . Contains ( "GoogleVRHMD" ) )
{
GoogleVRPluginEnabled = true ;
continue ;
}
// check if the EOSShared plugin was enabled
if ( Plugin . Contains ( "EOSSDK" ) )
{
EOSSDKPluginEnabled = true ;
continue ;
}
}
2022-05-25 19:55:37 -04:00
UPL = new UnrealPluginLanguage ( ProjectFile , inPluginExtraData , NDKArches , "http://schemas.android.com/apk/res/android" , "xmlns:android=\"http://schemas.android.com/apk/res/android\"" , UnrealTargetPlatform . Android , Logger ) ;
2022-05-24 19:41:41 -04:00
UPLHashCode = UPL . GetUPLHash ( ) ;
// APL.SetTrace();
}
private void SetMinimumSDKLevelForGradle ( )
{
if ( FacebookPluginEnabled )
{
MinimumSDKLevelForGradle = Math . Max ( MinimumSDKLevelForGradle , 15 ) ;
}
if ( ARCorePluginEnabled )
{
MinimumSDKLevelForGradle = Math . Max ( MinimumSDKLevelForGradle , 19 ) ;
}
if ( EOSSDKPluginEnabled )
{
MinimumSDKLevelForGradle = Math . Max ( MinimumSDKLevelForGradle , 23 ) ;
}
}
/// <summary>
/// Simple function to pipe output asynchronously
/// </summary>
private void ParseApiLevel ( object Sender , DataReceivedEventArgs Event )
{
// DataReceivedEventHandler is fired with a null string when the output stream is closed. We don't want to
// print anything for that event.
if ( ! String . IsNullOrEmpty ( Event . Data ) )
{
string Line = Event . Data ;
if ( Line . StartsWith ( "id:" ) )
{
// the line should look like: id: 1 or "android-19"
string [ ] Tokens = Line . Split ( "\"" . ToCharArray ( ) ) ;
if ( Tokens . Length > = 2 )
{
PossibleApiLevels ! . Add ( Tokens [ 1 ] ) ;
}
}
}
}
private ConfigHierarchy GetConfigCacheIni ( ConfigHierarchyType Type )
{
return ConfigCache . ReadHierarchy ( Type , DirectoryReference . FromFile ( ProjectFile ) , UnrealTargetPlatform . Android ) ;
}
private bool ValidateSDK ( string PlatformsDir , string ApiString )
{
if ( ! Directory . Exists ( PlatformsDir ) )
{
return false ;
}
string SDKPlatformDir = Path . Combine ( PlatformsDir , ApiString ) ;
return Directory . Exists ( SDKPlatformDir ) ;
}
private int GetApiLevelInt ( string ApiString )
{
int VersionInt = 0 ;
if ( ApiString . Contains ( "-" ) )
{
int Version ;
if ( int . TryParse ( ApiString . Substring ( ApiString . LastIndexOf ( '-' ) + 1 ) , out Version ) )
{
VersionInt = Version ;
}
}
return VersionInt ;
}
private string? CachedSDKLevel = null ;
private string GetSdkApiLevel ( AndroidToolChain ToolChain )
{
if ( CachedSDKLevel = = null )
{
// ask the .ini system for what version to use
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
string SDKLevel ;
Ini . GetString ( "/Script/AndroidPlatformEditor.AndroidSDKSettings" , "SDKAPILevel" , out SDKLevel ) ;
// check for project override of SDK API level
string ProjectSDKLevel ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "SDKAPILevelOverride" , out ProjectSDKLevel ) ;
ProjectSDKLevel = ProjectSDKLevel . Trim ( ) ;
if ( ProjectSDKLevel ! = "" )
{
SDKLevel = ProjectSDKLevel ;
}
// if we want to use whatever version the ndk uses, then use that
if ( SDKLevel = = "matchndk" )
{
SDKLevel = ToolChain . GetNdkApiLevel ( ) ;
}
// run a command and capture output
if ( SDKLevel = = "latest" )
{
SDKLevel = ToolChain . GetLargestApiLevel ( ) ;
}
// make sure it is at least android-23
int SDKLevelInt = GetApiLevelInt ( SDKLevel ) ;
if ( SDKLevelInt < MinimumSDKLevel )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Requires at least SDK API level {MinimumSDKLevel}, currently set to '{SDKLevel}'" , MinimumSDKLevel , SDKLevel ) ;
2022-05-24 19:41:41 -04:00
SDKLevel = ToolChain . GetLargestApiLevel ( ) ;
SDKLevelInt = GetApiLevelInt ( SDKLevel ) ;
if ( SDKLevelInt < MinimumSDKLevel )
{
SDKLevelInt = MinimumSDKLevel ;
SDKLevel = "android-" + MinimumSDKLevel . ToString ( ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Gradle will attempt to download SDK API level {SDKLevelInt}" , SDKLevelInt ) ;
2022-05-24 19:41:41 -04:00
}
}
// validate the platform SDK is installed
string PlatformsDir = Environment . ExpandEnvironmentVariables ( "%ANDROID_HOME%/platforms" ) ;
if ( ! ValidateSDK ( PlatformsDir , SDKLevel ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "The SDK API requested '{SdkLevel}' not installed in {PlatformsDir}; Gradle will attempt to download it." , SDKLevel , PlatformsDir ) ;
2022-05-24 19:41:41 -04:00
}
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Building Java with SDK API level '{SDKLevel}'" , SDKLevel ) ;
2022-05-24 19:41:41 -04:00
CachedSDKLevel = SDKLevel ;
}
return CachedSDKLevel ;
}
private string? CachedBuildToolsVersion = null ;
private string? LastAndroidHomePath = null ;
private uint GetRevisionValue ( string VersionString )
{
if ( VersionString = = null )
{
return 0 ;
}
// read up to 4 sections (ie. 20.0.3.5), first section most significant
// each section assumed to be 0 to 255 range
uint Value = 0 ;
try
{
string [ ] Sections = VersionString . Split ( "." . ToCharArray ( ) ) ;
Value | = ( Sections . Length > 0 ) ? ( uint . Parse ( Sections [ 0 ] ) < < 24 ) : 0 ;
Value | = ( Sections . Length > 1 ) ? ( uint . Parse ( Sections [ 1 ] ) < < 16 ) : 0 ;
Value | = ( Sections . Length > 2 ) ? ( uint . Parse ( Sections [ 2 ] ) < < 8 ) : 0 ;
Value | = ( Sections . Length > 3 ) ? uint . Parse ( Sections [ 3 ] ) : 0 ;
}
catch ( Exception )
{
// ignore poorly formed version
}
return Value ;
}
private string GetBuildToolsVersion ( )
{
// return cached path if ANDROID_HOME has not changed
string HomePath = Environment . ExpandEnvironmentVariables ( "%ANDROID_HOME%" ) ;
if ( CachedBuildToolsVersion ! = null & & LastAndroidHomePath = = HomePath )
{
return CachedBuildToolsVersion ;
}
string? BestVersionString = null ;
uint BestVersion = 0 ;
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "BuildToolsOverride" , out BestVersionString ) ;
if ( BestVersionString = = null | | BestVersionString = = "" | | BestVersionString = = "latest" )
{
// get a list of the directories in build-tools.. may be more than one set installed (or none which is bad)
string [ ] Subdirs = Directory . GetDirectories ( Path . Combine ( HomePath , "build-tools" ) ) ;
if ( Subdirs . Length = = 0 )
{
throw new BuildException ( "Failed to find %ANDROID_HOME%/build-tools subdirectory. Run SDK manager and install build-tools." ) ;
}
// valid directories will have a source.properties with the Pkg.Revision (there is no guarantee we can use the directory name as revision)
foreach ( string CandidateDir in Subdirs )
{
string AaptFilename = Path . Combine ( CandidateDir , RuntimePlatform . IsWindows ? "aapt.exe" : "aapt" ) ;
string RevisionString = "" ;
uint RevisionValue = 0 ;
if ( File . Exists ( AaptFilename ) )
{
string SourcePropFilename = Path . Combine ( CandidateDir , "source.properties" ) ;
if ( File . Exists ( SourcePropFilename ) )
{
string [ ] PropertyContents = File . ReadAllLines ( SourcePropFilename ) ;
foreach ( string PropertyLine in PropertyContents )
{
if ( PropertyLine . StartsWith ( "Pkg.Revision=" ) )
{
RevisionString = PropertyLine . Substring ( 13 ) ;
RevisionValue = GetRevisionValue ( RevisionString ) ;
break ;
}
}
}
}
// remember it if newer version or haven't found one yet
if ( RevisionValue > BestVersion | | BestVersionString = = null )
{
BestVersion = RevisionValue ;
BestVersionString = RevisionString ;
}
}
}
if ( BestVersionString = = null )
{
BestVersionString = "30.0.3" ;
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "Failed to find %ANDROID_HOME%/build-tools subdirectory. Will attempt to use {BestVersionString}." , BestVersionString ) ;
2022-05-24 19:41:41 -04:00
}
BestVersion = GetRevisionValue ( BestVersionString ) ;
// with Gradle enabled use at least 28.0.3 (will be installed by Gradle if missing)
if ( BestVersion < ( ( 28 < < 24 ) | ( 0 < < 16 ) | ( 3 < < 8 ) ) )
{
BestVersionString = "28.0.3" ;
}
// don't allow higher than 30.0.3 for now (will be installed by Gradle if missing)
if ( BestVersion > ( ( 30 < < 24 ) | ( 0 < < 16 ) | ( 3 < < 8 ) ) )
{
BestVersionString = "30.0.3" ;
}
CachedBuildToolsVersion = BestVersionString ;
LastAndroidHomePath = HomePath ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Building with Build Tools version '{CachedBuildToolsVersion}'" , CachedBuildToolsVersion ) ;
2022-05-24 19:41:41 -04:00
return CachedBuildToolsVersion ;
}
public static string GetOBBVersionNumber ( int PackageVersion )
{
string VersionString = PackageVersion . ToString ( "0" ) ;
return VersionString ;
}
public bool GetPackageDataInsideApk ( )
{
return bPackageDataInsideApk ;
}
/// <summary>
/// Reads the bPackageDataInsideApk from AndroidRuntimeSettings
/// </summary>
/// <param name="Ini"></param>
protected bool ReadPackageDataInsideApkFromIni ( ConfigHierarchy ? Ini )
{
// make a new one if one wasn't passed in
if ( Ini = = null )
{
Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
}
// we check this a lot, so make it easy
bool bIniPackageDataInsideApk ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bPackageDataInsideApk" , out bIniPackageDataInsideApk ) ;
return bIniPackageDataInsideApk ;
}
public bool UseExternalFilesDir ( bool bDisallowExternalFilesDir , ConfigHierarchy ? Ini = null )
{
if ( bDisallowExternalFilesDir )
{
return false ;
}
// make a new one if one wasn't passed in
if ( Ini = = null )
{
Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
}
// we check this a lot, so make it easy
bool bUseExternalFilesDir ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bUseExternalFilesDir" , out bUseExternalFilesDir ) ;
return bUseExternalFilesDir ;
}
public bool IsPackagingForDaydream ( ConfigHierarchy ? Ini = null )
{
// always false if the GoogleVR plugin wasn't enabled
if ( ! GoogleVRPluginEnabled )
{
return false ;
}
// make a new one if one wasn't passed in
if ( Ini = = null )
{
Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
}
List < string > ? GoogleVRCaps = new List < string > ( ) ;
if ( Ini . GetArray ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "GoogleVRCaps" , out GoogleVRCaps ) )
{
return GoogleVRCaps . Contains ( "Daydream33" ) | | GoogleVRCaps . Contains ( "Daydream63" ) | | GoogleVRCaps . Contains ( "Daydream66" ) ;
}
else
{
// the default values for the VRCaps are Cardboard and Daydream33, so unless the
// developer changes the mode, there will be no setting string to look up here
return true ;
}
}
public List < string > GetTargetOculusMobileDevices ( ConfigHierarchy ? Ini = null )
{
// always false if the Oculus Mobile plugin wasn't enabled
if ( ! OculusMobilePluginEnabled )
{
return new List < string > ( ) ;
}
// make a new one if one wasn't passed in
if ( Ini = = null )
{
Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
}
List < string > ? OculusMobileDevices ;
bool result = Ini . GetArray ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "PackageForOculusMobile" , out OculusMobileDevices ) ;
if ( ! result | | OculusMobileDevices = = null )
{
OculusMobileDevices = new List < string > ( ) ;
}
return OculusMobileDevices ;
}
public bool IsPackagingForOculusMobile ( ConfigHierarchy ? Ini = null )
{
List < string > TargetOculusDevices = GetTargetOculusMobileDevices ( Ini ) ;
bool bTargetOculusDevices = ( TargetOculusDevices ! = null & & TargetOculusDevices . Count ( ) > 0 ) ;
return bTargetOculusDevices ;
}
public bool DisableVerifyOBBOnStartUp ( ConfigHierarchy ? Ini = null )
{
// make a new one if one wasn't passed in
if ( Ini = = null )
{
Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
}
// we check this a lot, so make it easy
bool bDisableVerifyOBBOnStartUp ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bDisableVerifyOBBOnStartUp" , out bDisableVerifyOBBOnStartUp ) ;
return bDisableVerifyOBBOnStartUp ;
}
private static bool SafeDeleteFile ( string Filename , bool bCheckExists = true )
{
if ( ! bCheckExists | | File . Exists ( Filename ) )
{
try
{
File . SetAttributes ( Filename , FileAttributes . Normal ) ;
File . Delete ( Filename ) ;
return true ;
}
catch ( System . UnauthorizedAccessException )
{
throw new BuildException ( "File '{0}' is in use; unable to modify it." , Filename ) ;
}
catch ( System . Exception )
{
return false ;
}
}
return true ;
}
private static void CopyFileDirectory ( string SourceDir , string DestDir , Dictionary < string , string > ? Replacements = null , string [ ] ? Excludes = null )
{
if ( ! Directory . Exists ( SourceDir ) )
{
return ;
}
string [ ] Files = Directory . GetFiles ( SourceDir , "*.*" , SearchOption . AllDirectories ) ;
foreach ( string Filename in Files )
{
if ( Excludes ! = null )
{
// skip files in excluded directories
string DirectoryName = Path . GetFileName ( Path . GetDirectoryName ( Filename ) ) ! ;
bool bExclude = false ;
foreach ( string Exclude in Excludes )
{
if ( DirectoryName = = Exclude )
{
bExclude = true ;
break ;
}
}
if ( bExclude )
{
continue ;
}
}
// skip template files
if ( Path . GetExtension ( Filename ) = = ".template" )
{
continue ;
}
// make the dst filename with the same structure as it was in SourceDir
string DestFilename = Path . Combine ( DestDir , Utils . MakePathRelativeTo ( Filename , SourceDir ) ) . Replace ( '\\' , Path . DirectorySeparatorChar ) . Replace ( '/' , Path . DirectorySeparatorChar ) ;
// make the subdirectory if needed
string DestSubdir = Path . GetDirectoryName ( DestFilename ) ! ;
if ( ! Directory . Exists ( DestSubdir ) )
{
Directory . CreateDirectory ( DestSubdir ) ;
}
// some files are handled specially
string Ext = Path . GetExtension ( Filename ) ;
if ( Ext = = ".xml" & & Replacements ! = null )
{
string Contents = File . ReadAllText ( Filename ) ;
// replace some variables
foreach ( KeyValuePair < string , string > Pair in Replacements )
{
Contents = Contents . Replace ( Pair . Key , Pair . Value ) ;
}
bool bWriteFile = true ;
if ( File . Exists ( DestFilename ) )
{
string OriginalContents = File . ReadAllText ( DestFilename ) ;
if ( Contents = = OriginalContents )
{
bWriteFile = false ;
}
}
// write out file if different
if ( bWriteFile )
{
SafeDeleteFile ( DestFilename ) ;
File . WriteAllText ( DestFilename , Contents ) ;
}
}
else
{
SafeDeleteFile ( DestFilename ) ;
File . Copy ( Filename , DestFilename ) ;
// preserve timestamp and clear read-only flags
FileInfo DestFileInfo = new FileInfo ( DestFilename ) ;
DestFileInfo . Attributes = DestFileInfo . Attributes & ~ FileAttributes . ReadOnly ;
File . SetLastWriteTimeUtc ( DestFilename , File . GetLastWriteTimeUtc ( Filename ) ) ;
}
}
}
2022-05-25 19:55:37 -04:00
private static void DeleteDirectory ( string InPath , ILogger Logger , string SubDirectoryToKeep = "" )
2022-05-24 19:41:41 -04:00
{
// skip the dir we want to
if ( String . Compare ( Path . GetFileName ( InPath ) , SubDirectoryToKeep , true ) = = 0 )
{
return ;
}
// delete all files in here
string [ ] Files ;
try
{
Files = Directory . GetFiles ( InPath ) ;
}
catch ( Exception )
{
// directory doesn't exist so all is good
return ;
}
foreach ( string Filename in Files )
{
try
{
// remove any read only flags
FileInfo FileInfo = new FileInfo ( Filename ) ;
FileInfo . Attributes = FileInfo . Attributes & ~ FileAttributes . ReadOnly ;
FileInfo . Delete ( ) ;
}
catch ( Exception )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Failed to delete all files in directory {InPath}. Continuing on..." , InPath ) ;
2022-05-24 19:41:41 -04:00
}
}
string [ ] Dirs = Directory . GetDirectories ( InPath , "*.*" , SearchOption . TopDirectoryOnly ) ;
foreach ( string Dir in Dirs )
{
2022-05-25 19:55:37 -04:00
DeleteDirectory ( Dir , Logger , SubDirectoryToKeep ) ;
2022-05-24 19:41:41 -04:00
// try to delete the directory, but allow it to fail (due to SubDirectoryToKeep still existing)
try
{
Directory . Delete ( Dir ) ;
}
catch ( Exception )
{
// do nothing
}
}
}
private bool BinaryFileEquals ( string SourceFilename , string DestFilename )
{
if ( ! File . Exists ( SourceFilename ) )
{
return false ;
}
if ( ! File . Exists ( DestFilename ) )
{
return false ;
}
FileInfo SourceInfo = new FileInfo ( SourceFilename ) ;
FileInfo DestInfo = new FileInfo ( DestFilename ) ;
if ( SourceInfo . Length ! = DestInfo . Length )
{
return false ;
}
using ( FileStream SourceStream = new FileStream ( SourceFilename , FileMode . Open , FileAccess . Read , FileShare . Read ) )
using ( BinaryReader SourceReader = new BinaryReader ( SourceStream ) )
using ( FileStream DestStream = new FileStream ( DestFilename , FileMode . Open , FileAccess . Read , FileShare . Read ) )
using ( BinaryReader DestReader = new BinaryReader ( DestStream ) )
{
while ( true )
{
byte [ ] SourceData = SourceReader . ReadBytes ( 4096 ) ;
byte [ ] DestData = DestReader . ReadBytes ( 4096 ) ;
if ( SourceData . Length ! = DestData . Length )
{
return false ;
}
if ( SourceData . Length = = 0 )
{
return true ;
}
if ( ! SourceData . SequenceEqual ( DestData ) )
{
return false ;
}
}
}
}
private bool CopyIfDifferent ( string SourceFilename , string DestFilename , bool bLog , bool bContentCompare )
{
if ( ! File . Exists ( SourceFilename ) )
{
return false ;
}
bool bDestFileAlreadyExists = File . Exists ( DestFilename ) ;
bool bNeedCopy = ! bDestFileAlreadyExists ;
if ( ! bNeedCopy )
{
if ( bContentCompare )
{
bNeedCopy = ! BinaryFileEquals ( SourceFilename , DestFilename ) ;
}
else
{
FileInfo SourceInfo = new FileInfo ( SourceFilename ) ;
FileInfo DestInfo = new FileInfo ( DestFilename ) ;
if ( SourceInfo . Length ! = DestInfo . Length )
{
bNeedCopy = true ;
}
else if ( File . GetLastWriteTimeUtc ( DestFilename ) < File . GetLastWriteTimeUtc ( SourceFilename ) )
{
// destination file older than source
bNeedCopy = true ;
}
}
}
if ( bNeedCopy )
{
if ( bLog )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Copying {SourceFilename} to {DestFilename}" , SourceFilename , DestFilename ) ;
2022-05-24 19:41:41 -04:00
}
if ( bDestFileAlreadyExists )
{
SafeDeleteFile ( DestFilename , false ) ;
}
File . Copy ( SourceFilename , DestFilename ) ;
File . SetLastWriteTimeUtc ( DestFilename , File . GetLastWriteTimeUtc ( SourceFilename ) ) ;
// did copy
return true ;
}
// did not copy
return false ;
}
private void CleanCopyDirectory ( string SourceDir , string DestDir , string [ ] ? Excludes = null )
{
if ( ! Directory . Exists ( SourceDir ) )
{
return ;
}
if ( ! Directory . Exists ( DestDir ) )
{
CopyFileDirectory ( SourceDir , DestDir , null , Excludes ) ;
return ;
}
// copy files that are different and make a list of ones to keep
string [ ] StartingSourceFiles = Directory . GetFiles ( SourceDir , "*.*" , SearchOption . AllDirectories ) ;
List < string > FilesToKeep = new List < string > ( ) ;
foreach ( string Filename in StartingSourceFiles )
{
if ( Excludes ! = null )
{
// skip files in excluded directories
string DirectoryName = Path . GetFileName ( Path . GetDirectoryName ( Filename ) ) ! ;
bool bExclude = false ;
foreach ( string Exclude in Excludes )
{
if ( DirectoryName = = Exclude )
{
bExclude = true ;
break ;
}
}
if ( bExclude )
{
continue ;
}
}
// make the dest filename with the same structure as it was in SourceDir
string DestFilename = Path . Combine ( DestDir , Utils . MakePathRelativeTo ( Filename , SourceDir ) ) ;
// remember this file to keep
FilesToKeep . Add ( DestFilename ) ;
// only copy files that are new or different
if ( FilesAreDifferent ( Filename , DestFilename ) )
{
if ( File . Exists ( DestFilename ) )
{
// xml files may have been rewritten but contents still the same so check contents also
string Ext = Path . GetExtension ( Filename ) ;
if ( Ext = = ".xml" )
{
if ( File . ReadAllText ( Filename ) = = File . ReadAllText ( DestFilename ) )
{
continue ;
}
}
// delete it so can copy over it
SafeDeleteFile ( DestFilename ) ;
}
// make the subdirectory if needed
string DestSubdir = Path . GetDirectoryName ( DestFilename ) ! ;
if ( ! Directory . Exists ( DestSubdir ) )
{
Directory . CreateDirectory ( DestSubdir ) ;
}
// copy it
File . Copy ( Filename , DestFilename ) ;
// preserve timestamp and clear read-only flags
FileInfo DestFileInfo = new FileInfo ( DestFilename ) ;
DestFileInfo . Attributes = DestFileInfo . Attributes & ~ FileAttributes . ReadOnly ;
File . SetLastWriteTimeUtc ( DestFilename , File . GetLastWriteTimeUtc ( Filename ) ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Copied file {DestFilename}." , DestFilename ) ;
2022-05-24 19:41:41 -04:00
}
}
// delete any files not in the keep list
string [ ] StartingDestFiles = Directory . GetFiles ( DestDir , "*.*" , SearchOption . AllDirectories ) ;
foreach ( string Filename in StartingDestFiles )
{
if ( ! FilesToKeep . Contains ( Filename ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Deleting unneeded file {Filename}." , Filename ) ;
2022-05-24 19:41:41 -04:00
SafeDeleteFile ( Filename ) ;
}
}
// delete any empty directories
try
{
IEnumerable < string > BaseDirectories = Directory . EnumerateDirectories ( DestDir , "*" , SearchOption . AllDirectories ) . OrderByDescending ( x = > x ) ;
foreach ( string directory in BaseDirectories )
{
if ( Directory . Exists ( directory ) & & Directory . GetFiles ( directory , "*.*" , SearchOption . AllDirectories ) . Count ( ) = = 0 )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Cleaning Directory {Directory} as empty." , directory ) ;
2022-05-24 19:41:41 -04:00
Directory . Delete ( directory , true ) ;
}
}
}
catch ( Exception )
{
// likely System.IO.DirectoryNotFoundException, ignore it
}
}
public string GetUnrealBuildFilePath ( String EngineDirectory )
{
return Path . GetFullPath ( Path . Combine ( EngineDirectory , "Build/Android/Java" ) ) ;
}
public string GetUnrealJavaSrcPath ( )
{
return Path . Combine ( "src" , "com" , "epicgames" , "unreal" ) ;
}
public string GetUnrealJavaFilePath ( String EngineDirectory )
{
return Path . GetFullPath ( Path . Combine ( GetUnrealBuildFilePath ( EngineDirectory ) , GetUnrealJavaSrcPath ( ) ) ) ;
}
public string GetUnrealJavaBuildSettingsFileName ( String EngineDirectory )
{
return Path . Combine ( GetUnrealJavaFilePath ( EngineDirectory ) , "JavaBuildSettings.java" ) ;
}
public string GetUnrealJavaDownloadShimFileName ( string Directory )
{
return Path . Combine ( Directory , "DownloadShim.java" ) ;
}
public string GetUnrealTemplateJavaSourceDir ( string Directory )
{
return Path . Combine ( GetUnrealBuildFilePath ( Directory ) , "JavaTemplates" ) ;
}
public string GetUnrealTemplateJavaDestination ( string Directory , string FileName )
{
return Path . Combine ( Directory , FileName ) ;
}
public string GetUnrealJavaOBBDataFileName ( string Directory )
{
return Path . Combine ( Directory , "OBBData.java" ) ;
}
public class TemplateFile
{
public string SourceFile ;
public string DestinationFile ;
public TemplateFile ( string SourceFile , string DestinationFile )
{
this . SourceFile = SourceFile ;
this . DestinationFile = DestinationFile ;
}
}
private void MakeDirectoryIfRequired ( string DestFilename )
{
string DestSubdir = Path . GetDirectoryName ( DestFilename ) ! ;
if ( ! Directory . Exists ( DestSubdir ) )
{
Directory . CreateDirectory ( DestSubdir ) ;
}
}
private int CachedStoreVersion = - 1 ;
private int CachedStoreVersionOffsetArmV7 = 0 ;
private int CachedStoreVersionOffsetArm64 = 0 ;
private int CachedStoreVersionOffsetX8664 = 0 ;
public int GetStoreVersion ( string UnrealArch )
{
if ( CachedStoreVersion < 1 )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
int StoreVersion = 1 ;
Ini . GetInt32 ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "StoreVersion" , out StoreVersion ) ;
bool bUseChangeListAsStoreVersion = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bUseChangeListAsStoreVersion" , out bUseChangeListAsStoreVersion ) ;
bool IsBuildMachine = Environment . GetEnvironmentVariable ( "IsBuildMachine" ) = = "1" ;
// override store version with changelist if enabled and is build machine
if ( bUseChangeListAsStoreVersion & & IsBuildMachine )
{
// make sure changelist is cached (clear unused warning)
string EngineVersion = ReadEngineVersion ( ) ;
if ( EngineVersion = = null )
{
throw new BuildException ( "No engine version!" ) ;
}
int Changelist = 0 ;
if ( int . TryParse ( EngineChangelist , out Changelist ) )
{
if ( Changelist ! = 0 )
{
StoreVersion = Changelist ;
}
}
}
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "GotStoreVersion found v{StoreVersion}. (bUseChangeListAsStoreVersion={bUseChangeListAsStoreVersion} IsBuildMachine={IsBuildMachine} EngineChangeList={EngineChangeList})" , StoreVersion , bUseChangeListAsStoreVersion , IsBuildMachine , EngineChangelist ) ;
2022-05-24 19:41:41 -04:00
CachedStoreVersion = StoreVersion ;
Ini . GetInt32 ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "StoreVersionOffsetArmV7" , out CachedStoreVersionOffsetArmV7 ) ;
Ini . GetInt32 ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "StoreVersionOffsetArm64" , out CachedStoreVersionOffsetArm64 ) ;
Ini . GetInt32 ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "StoreVersionOffsetX8664" , out CachedStoreVersionOffsetX8664 ) ;
}
switch ( UnrealArch )
{
case "-armv7" : return CachedStoreVersion + CachedStoreVersionOffsetArmV7 ;
case "-arm64" : return CachedStoreVersion + CachedStoreVersionOffsetArm64 ;
case "-x64" : return CachedStoreVersion + CachedStoreVersionOffsetX8664 ;
}
return CachedStoreVersion ;
}
private string? CachedVersionDisplayName = null ;
public string GetVersionDisplayName ( bool bIsEmbedded )
{
if ( string . IsNullOrEmpty ( CachedVersionDisplayName ) )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
string VersionDisplayName = "" ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "VersionDisplayName" , out VersionDisplayName ) ;
if ( Environment . GetEnvironmentVariable ( "IsBuildMachine" ) = = "1" )
{
bool bAppendChangeListToVersionDisplayName = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bAppendChangeListToVersionDisplayName" , out bAppendChangeListToVersionDisplayName ) ;
if ( bAppendChangeListToVersionDisplayName )
{
VersionDisplayName = string . Format ( "{0}-{1}" , VersionDisplayName , EngineChangelist ) ;
}
bool bAppendPlatformToVersionDisplayName = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bAppendPlatformToVersionDisplayName" , out bAppendPlatformToVersionDisplayName ) ;
if ( bAppendPlatformToVersionDisplayName )
{
VersionDisplayName = string . Format ( "{0}-Android" , VersionDisplayName ) ;
}
// append optional text to version name if embedded build
if ( bIsEmbedded )
{
string EmbeddedAppendDisplayName = "" ;
if ( Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "EmbeddedAppendDisplayName" , out EmbeddedAppendDisplayName ) )
{
VersionDisplayName = VersionDisplayName + EmbeddedAppendDisplayName ;
}
}
}
CachedVersionDisplayName = VersionDisplayName ;
}
return CachedVersionDisplayName ;
}
public void WriteJavaOBBDataFile ( string FileName , string PackageName , List < string > ObbSources , string CookFlavor , bool bPackageDataInsideApk , string UnrealArch )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n==== Writing to OBB data file {FileName} ====" , FileName ) ;
2022-05-24 19:41:41 -04:00
// always must write if file does not exist
bool bFileExists = File . Exists ( FileName ) ;
bool bMustWriteFile = ! bFileExists ;
string AppType = "" ;
if ( CookFlavor . EndsWith ( "Client" ) )
{
// AppType = ".Client"; // should always be empty now; fix up the name in batch file instead
}
int StoreVersion = GetStoreVersion ( UnrealArch ) ;
StringBuilder obbData = new StringBuilder ( "package " + PackageName + ";\n\n" ) ;
obbData . Append ( "public class OBBData\n{\n" ) ;
obbData . Append ( "public static final String AppType = \"" + AppType + "\";\n\n" ) ;
obbData . Append ( "public static class XAPKFile {\npublic final boolean mIsMain;\npublic final String mFileVersion;\n" ) ;
obbData . Append ( "public final long mFileSize;\nXAPKFile(boolean isMain, String fileVersion, long fileSize) {\nmIsMain = isMain;\nmFileVersion = fileVersion;\nmFileSize = fileSize;\n" ) ;
obbData . Append ( "}\n}\n\n" ) ;
// write the data here
obbData . Append ( "public static final XAPKFile[] xAPKS = {\n" ) ;
// For each obb file... but we only have one... for now anyway.
bool first = ObbSources . Count > 1 ;
bool AnyOBBExists = false ;
foreach ( string ObbSource in ObbSources )
{
bool bOBBExists = File . Exists ( ObbSource ) ;
AnyOBBExists | = bOBBExists ;
obbData . Append ( "new XAPKFile(\n" + ( ObbSource . Contains ( ".patch." ) ? "false, // false signifies a patch file\n" : "true, // true signifies a main file\n" ) ) ;
obbData . AppendFormat ( "\"{0}\", // the version of the APK that the file was uploaded against\n" , GetOBBVersionNumber ( StoreVersion ) ) ;
obbData . AppendFormat ( "{0}L // the length of the file in bytes\n" , bOBBExists ? new FileInfo ( ObbSource ) . Length : 0 ) ;
obbData . AppendFormat ( "){0}\n" , first ? "," : "" ) ;
first = false ;
}
obbData . Append ( "};\n" ) ; // close off data
obbData . Append ( "};\n" ) ; // close class definition off
// see if we need to replace the file if it exists
if ( ! bMustWriteFile & & bFileExists )
{
string [ ] obbDataFile = File . ReadAllLines ( FileName ) ;
// Must always write if AppType not defined
bool bHasAppType = false ;
foreach ( string FileLine in obbDataFile )
{
if ( FileLine . Contains ( "AppType =" ) )
{
bHasAppType = true ;
break ;
}
}
if ( ! bHasAppType )
{
bMustWriteFile = true ;
}
// OBB must exist, contents must be different, and not packaging in APK to require replacing
if ( ! bMustWriteFile & & AnyOBBExists & & ! bPackageDataInsideApk & & ! obbDataFile . SequenceEqual ( ( obbData . ToString ( ) ) . Split ( '\n' ) ) )
{
bMustWriteFile = true ;
}
}
if ( bMustWriteFile )
{
MakeDirectoryIfRequired ( FileName ) ;
using ( StreamWriter outputFile = new StreamWriter ( FileName , false ) )
{
string [ ] obbSrc = obbData . ToString ( ) . Split ( '\n' ) ;
foreach ( string line in obbSrc )
{
outputFile . WriteLine ( line ) ;
}
}
}
else
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n==== OBB data file up to date so not writing. ====" ) ;
2022-05-24 19:41:41 -04:00
}
}
public void WriteJavaDownloadSupportFiles ( string ShimFileName , IEnumerable < TemplateFile > TemplateFiles , Dictionary < string , string > replacements )
{
// Deal with the Shim first as that is a known target and is easy to deal with
// If it exists then read it
string [ ] ? DestFileContent = File . Exists ( ShimFileName ) ? File . ReadAllLines ( ShimFileName ) : null ;
StringBuilder ShimFileContent = new StringBuilder ( "package com.epicgames.unreal;\n\n" ) ;
ShimFileContent . AppendFormat ( "import {0}.OBBDownloaderService;\n" , replacements [ "$$PackageName$$" ] ) ;
ShimFileContent . AppendFormat ( "import {0}.DownloaderActivity;\n" , replacements [ "$$PackageName$$" ] ) ;
// Do OBB file checking without using DownloadActivity to avoid transit to another activity
ShimFileContent . Append ( "import android.app.Activity;\n" ) ;
ShimFileContent . Append ( "import com.google.android.vending.expansion.downloader.Helpers;\n" ) ;
ShimFileContent . AppendFormat ( "import {0}.OBBData;\n" , replacements [ "$$PackageName$$" ] ) ;
ShimFileContent . Append ( "\n\npublic class DownloadShim\n{\n" ) ;
ShimFileContent . Append ( "\tpublic static OBBDownloaderService DownloaderService;\n" ) ;
ShimFileContent . Append ( "\tpublic static DownloaderActivity DownloadActivity;\n" ) ;
ShimFileContent . Append ( "\tpublic static Class<DownloaderActivity> GetDownloaderType() { return DownloaderActivity.class; }\n" ) ;
// Do OBB file checking without using DownloadActivity to avoid transit to another activity
ShimFileContent . Append ( "\tpublic static boolean expansionFilesDelivered(Activity activity, int version) {\n" ) ;
ShimFileContent . Append ( "\t\tfor (OBBData.XAPKFile xf : OBBData.xAPKS) {\n" ) ;
ShimFileContent . Append ( "\t\t\tString fileName = Helpers.getExpansionAPKFileName(activity, xf.mIsMain, Integer.toString(version), OBBData.AppType);\n" ) ;
ShimFileContent . Append ( "\t\t\tGameActivity.Log.debug(\"Checking for file : \" + fileName);\n" ) ;
ShimFileContent . Append ( "\t\t\tString fileForNewFile = Helpers.generateSaveFileName(activity, fileName);\n" ) ;
ShimFileContent . Append ( "\t\t\tString fileForDevFile = Helpers.generateSaveFileNameDevelopment(activity, fileName);\n" ) ;
ShimFileContent . Append ( "\t\t\tGameActivity.Log.debug(\"which is really being resolved to : \" + fileForNewFile + \"\\n Or : \" + fileForDevFile);\n" ) ;
ShimFileContent . Append ( "\t\t\tif (Helpers.doesFileExist(activity, fileName, xf.mFileSize, false)) {\n" ) ;
ShimFileContent . Append ( "\t\t\t\tGameActivity.Log.debug(\"Found OBB here: \" + fileForNewFile);\n" ) ;
ShimFileContent . Append ( "\t\t\t}\n" ) ;
ShimFileContent . Append ( "\t\t\telse if (Helpers.doesFileExistDev(activity, fileName, xf.mFileSize, false)) {\n" ) ;
ShimFileContent . Append ( "\t\t\t\tGameActivity.Log.debug(\"Found OBB here: \" + fileForDevFile);\n" ) ;
ShimFileContent . Append ( "\t\t\t}\n" ) ;
ShimFileContent . Append ( "\t\t\telse return false;\n" ) ;
ShimFileContent . Append ( "\t\t}\n" ) ;
ShimFileContent . Append ( "\t\treturn true;\n" ) ;
ShimFileContent . Append ( "\t}\n" ) ;
ShimFileContent . Append ( "}\n" ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n==== Writing to shim file {ShimFileName} ====" , ShimFileName ) ;
2022-05-24 19:41:41 -04:00
// If they aren't the same then dump out the settings
if ( DestFileContent = = null | | ! DestFileContent . SequenceEqual ( ( ShimFileContent . ToString ( ) ) . Split ( '\n' ) ) )
{
MakeDirectoryIfRequired ( ShimFileName ) ;
using ( StreamWriter outputFile = new StreamWriter ( ShimFileName , false ) )
{
string [ ] shimSrc = ShimFileContent . ToString ( ) . Split ( '\n' ) ;
foreach ( string line in shimSrc )
{
outputFile . WriteLine ( line ) ;
}
}
}
else
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n==== Shim data file up to date so not writing. ====" ) ;
2022-05-24 19:41:41 -04:00
}
// Now we move on to the template files
foreach ( TemplateFile template in TemplateFiles )
{
string [ ] templateSrc = File . ReadAllLines ( template . SourceFile ) ;
string [ ] ? templateDest = File . Exists ( template . DestinationFile ) ? File . ReadAllLines ( template . DestinationFile ) : null ;
for ( int i = 0 ; i < templateSrc . Length ; + + i )
{
string srcLine = templateSrc [ i ] ;
bool changed = false ;
foreach ( KeyValuePair < string , string > kvp in replacements )
{
if ( srcLine . Contains ( kvp . Key ) )
{
srcLine = srcLine . Replace ( kvp . Key , kvp . Value ) ;
changed = true ;
}
}
if ( changed )
{
templateSrc [ i ] = srcLine ;
}
}
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n==== Writing to template target file {File} ====" , template . DestinationFile ) ;
2022-05-24 19:41:41 -04:00
if ( templateDest = = null | | templateSrc . Length ! = templateDest . Length | | ! templateSrc . SequenceEqual ( templateDest ) )
{
MakeDirectoryIfRequired ( template . DestinationFile ) ;
using ( StreamWriter outputFile = new StreamWriter ( template . DestinationFile , false ) )
{
foreach ( string line in templateSrc )
{
outputFile . WriteLine ( line ) ;
}
}
}
else
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n==== Template target file up to date so not writing. ====" ) ;
2022-05-24 19:41:41 -04:00
}
}
}
public void WriteCrashlyticsResources ( string UEBuildPath , string PackageName , string ApplicationDisplayName , bool bIsEmbedded , string UnrealArch )
{
System . DateTime CurrentDateTime = System . DateTime . Now ;
string BuildID = Guid . NewGuid ( ) . ToString ( ) ;
string VersionDisplayName = GetVersionDisplayName ( bIsEmbedded ) ;
StringBuilder CrashPropertiesContent = new StringBuilder ( "" ) ;
CrashPropertiesContent . Append ( "# This file is automatically generated by Crashlytics to uniquely\n" ) ;
CrashPropertiesContent . Append ( "# identify individual builds of your Android application.\n" ) ;
CrashPropertiesContent . Append ( "#\n" ) ;
CrashPropertiesContent . Append ( "# Do NOT modify, delete, or commit to source control!\n" ) ;
CrashPropertiesContent . Append ( "#\n" ) ;
CrashPropertiesContent . Append ( "# " + CurrentDateTime . ToString ( "D" ) + "\n" ) ;
CrashPropertiesContent . Append ( "version_name=" + VersionDisplayName + "\n" ) ;
CrashPropertiesContent . Append ( "package_name=" + PackageName + "\n" ) ;
CrashPropertiesContent . Append ( "build_id=" + BuildID + "\n" ) ;
CrashPropertiesContent . Append ( "version_code=" + GetStoreVersion ( UnrealArch ) . ToString ( ) + "\n" ) ;
string CrashPropertiesFileName = Path . Combine ( UEBuildPath , "assets" , "crashlytics-build.properties" ) ;
MakeDirectoryIfRequired ( CrashPropertiesFileName ) ;
File . WriteAllText ( CrashPropertiesFileName , CrashPropertiesContent . ToString ( ) ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "==== Write {CrashPropertiesFileName} ====" , CrashPropertiesFileName ) ;
2022-05-24 19:41:41 -04:00
StringBuilder BuildIDContent = new StringBuilder ( "" ) ;
BuildIDContent . Append ( "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n" ) ;
BuildIDContent . Append ( "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n" ) ;
BuildIDContent . Append ( "<!--\n" ) ;
BuildIDContent . Append ( " This file is automatically generated by Crashlytics to uniquely\n" ) ;
BuildIDContent . Append ( " identify individual builds of your Android application.\n" ) ;
BuildIDContent . Append ( "\n" ) ;
BuildIDContent . Append ( " Do NOT modify, delete, or commit to source control!\n" ) ;
BuildIDContent . Append ( "-->\n" ) ;
BuildIDContent . Append ( "<string tools:ignore=\"UnusedResources, TypographyDashes\" name=\"com.crashlytics.android.build_id\" translatable=\"false\">" + BuildID + "</string>\n" ) ;
BuildIDContent . Append ( "</resources>\n" ) ;
string BuildIDFileName = Path . Combine ( UEBuildPath , "res" , "values" , "com_crashlytics_build_id.xml" ) ;
MakeDirectoryIfRequired ( BuildIDFileName ) ;
File . WriteAllText ( BuildIDFileName , BuildIDContent . ToString ( ) ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "==== Write {BuildIDFileName} ====" , BuildIDFileName ) ;
2022-05-24 19:41:41 -04:00
}
private static string GetNDKArch ( string UnrealArch )
{
switch ( UnrealArch )
{
case "-arm64" : return "arm64-v8a" ;
case "-x64" : return "x86_64" ;
default : throw new BuildException ( "Unknown Unreal architecture {0}" , UnrealArch ) ;
}
}
public static string GetUnrealArch ( string NDKArch )
{
switch ( NDKArch )
{
case "arm64-v8a" : return "-arm64" ;
case "arm64" : return "-arm64" ;
case "x86_64" :
case "x64" : return "-x64" ;
// default: throw new BuildException("Unknown NDK architecture '{0}'", NDKArch);
// future-proof by returning arm64 for unknown
default : return "-arm64" ;
}
}
2022-05-25 19:55:37 -04:00
private static void StripDebugSymbols ( string SourceFileName , string TargetFileName , string UnrealArch , ILogger Logger , bool bStripAll = false )
2022-05-24 19:41:41 -04:00
{
// Copy the file and remove read-only if necessary
File . Copy ( SourceFileName , TargetFileName , true ) ;
FileAttributes Attribs = File . GetAttributes ( TargetFileName ) ;
if ( Attribs . HasFlag ( FileAttributes . ReadOnly ) )
{
File . SetAttributes ( TargetFileName , Attribs & ~ FileAttributes . ReadOnly ) ;
}
ProcessStartInfo StartInfo = new ProcessStartInfo ( ) ;
StartInfo . FileName = AndroidToolChain . GetStripExecutablePath ( UnrealArch ) . Trim ( '"' ) ;
if ( bStripAll )
{
StartInfo . Arguments = "--strip-unneeded \"" + TargetFileName + "\"" ;
}
else
{
StartInfo . Arguments = "--strip-debug \"" + TargetFileName + "\"" ;
}
StartInfo . UseShellExecute = false ;
StartInfo . CreateNoWindow = true ;
2022-05-25 19:55:37 -04:00
Utils . RunLocalProcessAndLogOutput ( StartInfo , Logger ) ;
2022-05-24 19:41:41 -04:00
}
private static string GetPlatformNDKHostName ( )
{
if ( RuntimePlatform . IsLinux )
{
return "linux-x86_64" ;
}
else if ( RuntimePlatform . IsMac )
{
return "darwin-x86_64" ;
}
return "windows-x86_64" ;
}
private static void CopySTL ( AndroidToolChain ToolChain , string UnrealBuildPath , string UnrealArch , string NDKArch , bool bForDistribution )
{
// copy it in!
string SourceSTLSOName = Environment . ExpandEnvironmentVariables ( "%NDKROOT%/sources/cxx-stl/llvm-libc++/libs/" ) + NDKArch + "/libc++_shared.so" ;
if ( ! File . Exists ( SourceSTLSOName ) )
{
// NDK25 has changed a directory where it stores libs, check it instead
string NDKTargetTripletName = ( NDKArch = = "x86_64" ) ? "x86_64-linux-android" : "aarch64-linux-android" ;
SourceSTLSOName = Environment . ExpandEnvironmentVariables ( "%NDKROOT%/toolchains/llvm/prebuilt/" ) + GetPlatformNDKHostName ( ) + "/sysroot/usr/lib/" + NDKTargetTripletName + "/libc++_shared.so" ;
}
string FinalSTLSOName = UnrealBuildPath + "/jni/" + NDKArch + "/libc++_shared.so" ;
// check to see if libc++_shared.so is newer than last time we copied
bool bFileExists = File . Exists ( FinalSTLSOName ) ;
TimeSpan Diff = File . GetLastWriteTimeUtc ( FinalSTLSOName ) - File . GetLastWriteTimeUtc ( SourceSTLSOName ) ;
if ( ! bFileExists | | Diff . TotalSeconds < - 1 | | Diff . TotalSeconds > 1 )
{
SafeDeleteFile ( FinalSTLSOName ) ;
Directory . CreateDirectory ( Path . GetDirectoryName ( FinalSTLSOName ) ! ) ;
File . Copy ( SourceSTLSOName , FinalSTLSOName , true ) ;
// make sure it's writable if the source was readonly (e.g. autosdks)
new FileInfo ( FinalSTLSOName ) . IsReadOnly = false ;
File . SetLastWriteTimeUtc ( FinalSTLSOName , File . GetLastWriteTimeUtc ( SourceSTLSOName ) ) ;
}
}
private void CopyGfxDebugger ( string UnrealBuildPath , string UnrealArch , string NDKArch )
{
string AndroidGraphicsDebugger ;
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "AndroidGraphicsDebugger" , out AndroidGraphicsDebugger ) ;
switch ( AndroidGraphicsDebugger . ToLower ( ) )
{
case "mali" :
{
string MaliGraphicsDebuggerPath ;
AndroidPlatformSDK . GetPath ( Ini , "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "MaliGraphicsDebuggerPath" , out MaliGraphicsDebuggerPath ) ;
if ( Directory . Exists ( MaliGraphicsDebuggerPath ) )
{
Directory . CreateDirectory ( Path . Combine ( UnrealBuildPath , "libs" , NDKArch ) ) ;
string MaliLibSrcPath = Path . Combine ( MaliGraphicsDebuggerPath , "target" , "android-non-root" , "arm" , NDKArch , "libMGD.so" ) ;
if ( ! File . Exists ( MaliLibSrcPath ) )
{
// in v4.3.0 library location was changed
MaliLibSrcPath = Path . Combine ( MaliGraphicsDebuggerPath , "target" , "android" , "arm" , "unrooted" , NDKArch , "libMGD.so" ) ;
}
string MaliLibDstPath = Path . Combine ( UnrealBuildPath , "libs" , NDKArch , "libMGD.so" ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Copying {MaliLibSrcPath} to {MaliLibDstPath}" , MaliLibSrcPath , MaliLibDstPath ) ;
2022-05-24 19:41:41 -04:00
File . Copy ( MaliLibSrcPath , MaliLibDstPath , true ) ;
File . SetLastWriteTimeUtc ( MaliLibDstPath , File . GetLastWriteTimeUtc ( MaliLibSrcPath ) ) ;
string MaliVkLayerLibSrcPath = Path . Combine ( MaliGraphicsDebuggerPath , "target" , "android" , "arm" , "rooted" , NDKArch , "libGLES_aga.so" ) ;
if ( File . Exists ( MaliVkLayerLibSrcPath ) )
{
string MaliVkLayerLibDstPath = Path . Combine ( UnrealBuildPath , "libs" , NDKArch , "libVkLayerAGA.so" ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Copying {MaliVkLayerLibSrcPath} to {MaliVkLayerLibDstPath}" , MaliVkLayerLibSrcPath , MaliVkLayerLibDstPath ) ;
2022-05-24 19:41:41 -04:00
File . Copy ( MaliVkLayerLibSrcPath , MaliVkLayerLibDstPath , true ) ;
File . SetLastWriteTimeUtc ( MaliVkLayerLibDstPath , File . GetLastWriteTimeUtc ( MaliVkLayerLibSrcPath ) ) ;
}
}
}
break ;
// @TODO: Add NVIDIA Gfx Debugger
/ *
case "nvidia" :
{
Directory . CreateDirectory ( UnrealBuildPath + "/libs/" + NDKArch ) ;
File . Copy ( "F:/NVPACK/android-kk-egl-t124-a32/Stripped_libNvPmApi.Core.so" , UnrealBuildPath + "/libs/" + NDKArch + "/libNvPmApi.Core.so" , true ) ;
File . Copy ( "F:/NVPACK/android-kk-egl-t124-a32/Stripped_libNvidia_gfx_debugger.so" , UnrealBuildPath + "/libs/" + NDKArch + "/libNvidia_gfx_debugger.so" , true ) ;
}
break ;
* /
default :
break ;
}
}
void LogBuildSetup ( )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
bool bBuildForES31 = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bBuildForES31" , out bBuildForES31 ) ;
bool bSupportsVulkan = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bSupportsVulkan" , out bSupportsVulkan ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "bBuildForES31: {bBuildForES31}" , ( bBuildForES31 ? "true" : "false" ) ) ;
Logger . LogInformation ( "bSupportsVulkan: {bSupportsVulkan}" , ( bSupportsVulkan ? "true" : "false" ) ) ;
2022-05-24 19:41:41 -04:00
}
void CopyVulkanValidationLayers ( string UnrealBuildPath , string UnrealArch , string NDKArch , string Configuration )
{
bool bSupportsVulkan = false ;
bool bSupportsVulkanSM5 = false ;
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bSupportsVulkan" , out bSupportsVulkan ) ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bSupportsVulkanSM5" , out bSupportsVulkanSM5 ) ;
bool bCopyVulkanLayers = ( bSupportsVulkan | | bSupportsVulkanSM5 ) & & ( Configuration = = "Debug" | | Configuration = = "Development" ) ;
if ( bCopyVulkanLayers )
{
string VulkanLayersDir = Environment . ExpandEnvironmentVariables ( "%NDKROOT%/sources/third_party/vulkan/src/build-android/jniLibs/" ) + NDKArch ;
if ( Directory . Exists ( VulkanLayersDir ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Copying {ANDROID_VULKAN_VALIDATION_LAYER} vulkan layer from {VulkanLayersDir}" , ANDROID_VULKAN_VALIDATION_LAYER , VulkanLayersDir ) ;
2022-05-24 19:41:41 -04:00
string DestDir = Path . Combine ( UnrealBuildPath , "libs" , NDKArch ) ;
Directory . CreateDirectory ( DestDir ) ;
string SourceFilename = Path . Combine ( VulkanLayersDir , ANDROID_VULKAN_VALIDATION_LAYER ) ;
string DestFilename = Path . Combine ( DestDir , ANDROID_VULKAN_VALIDATION_LAYER ) ;
SafeDeleteFile ( DestFilename ) ;
File . Copy ( SourceFilename , DestFilename ) ;
FileInfo DestFileInfo = new FileInfo ( DestFilename ) ;
DestFileInfo . Attributes = DestFileInfo . Attributes & ~ FileAttributes . ReadOnly ;
File . SetLastWriteTimeUtc ( DestFilename , File . GetLastWriteTimeUtc ( SourceFilename ) ) ;
}
}
// Copy debug validation layers
if ( bSupportsVulkan & & Configuration ! = "Shipping" )
{
String LayerDir = "" ;
AndroidPlatformSDK . GetPath ( Ini , "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "DebugVulkanLayerDirectory" , out LayerDir ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "DebugVulkanLayerDirectory {LayerDir}" , LayerDir ) ;
2022-05-24 19:41:41 -04:00
if ( LayerDir ! = "" )
{
string VulkanLayersDir = Path . Combine ( Environment . ExpandEnvironmentVariables ( LayerDir ) , NDKArch ) ;
if ( Directory . Exists ( VulkanLayersDir ) )
{
string DestDir = Path . Combine ( UnrealBuildPath , "libs" , NDKArch ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Copying Debug vulkan layers from {VulkanLayersDir} to {DestDir}" , VulkanLayersDir , DestDir ) ;
2022-05-24 19:41:41 -04:00
if ( ! Directory . Exists ( DestDir ) )
{
Directory . CreateDirectory ( DestDir ) ;
}
CopyFileDirectory ( VulkanLayersDir , DestDir ) ;
}
}
}
}
void CopyClangSanitizerLib ( string UnrealBuildPath , string UnrealArch , string NDKArch , AndroidToolChain . ClangSanitizer Sanitizer )
{
string Architecture = "-aarch64" ;
switch ( NDKArch )
{
case "armeabi-v7a" :
Architecture = "-arm" ;
break ;
case "x86_64" :
Architecture = "-x86_64" ;
break ;
case "x86" :
Architecture = "-i686" ;
break ;
}
string LibName = "asan" ;
switch ( Sanitizer )
{
case AndroidToolChain . ClangSanitizer . HwAddress :
LibName = "hwasan" ;
break ;
case AndroidToolChain . ClangSanitizer . UndefinedBehavior :
LibName = "ubsan_standalone" ;
break ;
case AndroidToolChain . ClangSanitizer . UndefinedBehaviorMinimal :
LibName = "ubsan_minimal" ;
break ;
case AndroidToolChain . ClangSanitizer . Thread :
LibName = "thread" ;
break ;
}
string SanitizerFullLibName = "libclang_rt." + LibName + Architecture + "-android.so" ;
string WrapSh = Path . Combine ( Unreal . EngineDirectory . ToString ( ) , "Build" , "Android" , "ClangSanitizers" , "wrap.sh" ) ;
string PlatformHostName = GetPlatformNDKHostName ( ) ;
string VersionFileName = Path . Combine ( Environment . ExpandEnvironmentVariables ( "%NDKROOT%" ) , "toolchains" , "llvm" , "prebuilt" , PlatformHostName , "AndroidVersion.txt" ) ;
System . IO . StreamReader VersionFile = new System . IO . StreamReader ( VersionFileName ) ;
string LibsVersion = VersionFile . ReadLine ( ) ! ;
VersionFile . Close ( ) ;
string SanitizerLib = Path . Combine ( Environment . ExpandEnvironmentVariables ( "%NDKROOT%" ) , "toolchains" , "llvm" , "prebuilt" , PlatformHostName , "lib64" , "clang" , LibsVersion , "lib" , "linux" , SanitizerFullLibName ) ;
if ( File . Exists ( SanitizerLib ) & & File . Exists ( WrapSh ) )
{
string LibDestDir = Path . Combine ( UnrealBuildPath , "libs" , NDKArch ) ;
Directory . CreateDirectory ( LibDestDir ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Copying asan lib from {SanitizerLib} to {LibDestDir}" , SanitizerLib , LibDestDir ) ;
2022-05-24 19:41:41 -04:00
File . Copy ( SanitizerLib , Path . Combine ( LibDestDir , SanitizerFullLibName ) , true ) ;
string WrapDestDir = Path . Combine ( UnrealBuildPath , "resources" , "lib" , NDKArch ) ;
Directory . CreateDirectory ( WrapDestDir ) ;
string WrapDestFilePath = Path . Combine ( WrapDestDir , "wrap.sh" ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Copying wrap.sh from {WrapSh} to {WrapDestFilePath}" , WrapSh , WrapDestFilePath ) ;
2022-05-24 19:41:41 -04:00
File . Copy ( WrapSh , WrapDestFilePath , true ) ;
}
else
{
throw new BuildException ( "No asan lib found in {0} or wrap.sh in {1}" , SanitizerLib , WrapSh ) ;
}
}
2022-05-25 19:55:37 -04:00
private static int RunCommandLineProgramAndReturnResult ( string WorkingDirectory , string Command , string Params , ILogger Logger , string? OverrideDesc = null , bool bUseShellExecute = false )
2022-05-24 19:41:41 -04:00
{
// Process Arguments follow windows conventions in .NET Core
// Which means single quotes ' are not considered quotes.
// see https://github.com/dotnet/runtime/issues/29857
// also see UE-102580
// for rules see https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args
if ( Params . Contains ( "\'" ) )
{
Params = Params . Replace ( "\"" , "\\\"" ) ;
Params = Params . Replace ( '\'' , '\"' ) ;
}
if ( OverrideDesc = = null )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\nRunning: {Command} {Params}" , Command , Params ) ;
2022-05-24 19:41:41 -04:00
}
else if ( OverrideDesc ! = "" )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "{Message}" , OverrideDesc ) ;
Logger . LogDebug ( "\nRunning: {Command} {Params}" , Command , Params ) ;
2022-05-24 19:41:41 -04:00
}
ProcessStartInfo StartInfo = new ProcessStartInfo ( ) ;
StartInfo . WorkingDirectory = WorkingDirectory ;
StartInfo . FileName = Command ;
StartInfo . Arguments = Params ;
StartInfo . UseShellExecute = bUseShellExecute ;
StartInfo . WindowStyle = ProcessWindowStyle . Minimized ;
Process Proc = new Process ( ) ;
Proc . StartInfo = StartInfo ;
Proc . Start ( ) ;
Proc . WaitForExit ( ) ;
return Proc . ExitCode ;
}
2022-05-25 19:55:37 -04:00
private static void RunCommandLineProgramWithException ( string WorkingDirectory , string Command , string Params , ILogger Logger , string? OverrideDesc = null , bool bUseShellExecute = false )
2022-05-24 19:41:41 -04:00
{
// Process Arguments follow windows conventions in .NET Core
// Which means single quotes ' are not considered quotes.
// see https://github.com/dotnet/runtime/issues/29857
// also see UE-102580
// for rules see https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args
if ( Params . Contains ( "\'" ) )
{
Params = Params . Replace ( "\"" , "\\\"" ) ;
Params = Params . Replace ( '\'' , '\"' ) ;
}
if ( OverrideDesc = = null )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\nRunning: {Command} {Params}" , Command , Params ) ;
2022-05-24 19:41:41 -04:00
}
else if ( OverrideDesc ! = "" )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "{Message}" , OverrideDesc ) ;
Logger . LogDebug ( "\nRunning: {Command} {Params}" , Command , Params ) ;
2022-05-24 19:41:41 -04:00
}
ProcessStartInfo StartInfo = new ProcessStartInfo ( ) ;
StartInfo . WorkingDirectory = WorkingDirectory ;
StartInfo . FileName = Command ;
StartInfo . Arguments = Params ;
StartInfo . UseShellExecute = bUseShellExecute ;
StartInfo . WindowStyle = ProcessWindowStyle . Minimized ;
Process Proc = new Process ( ) ;
Proc . StartInfo = StartInfo ;
Proc . Start ( ) ;
Proc . WaitForExit ( ) ;
// android bat failure
if ( Proc . ExitCode ! = 0 )
{
throw new BuildException ( "{0} failed with args {1}" , Command , Params ) ;
}
}
private enum FilterAction
{
Skip ,
Replace ,
Error
}
private class FilterOperation
{
public FilterAction Action ;
public string Condition ;
public string Match ;
public string ReplaceWith ;
public FilterOperation ( FilterAction InAction , string InCondition , string InMatch , string InReplaceWith )
{
Action = InAction ;
Condition = InCondition ;
Match = InMatch ;
ReplaceWith = InReplaceWith ;
}
}
static private List < FilterOperation > ? ActiveStdOutFilter = null ;
static List < string > ParseCSVString ( string Input )
{
List < string > Results = new List < string > ( ) ;
StringBuilder WorkString = new StringBuilder ( ) ;
int FinalIndex = Input . Length ;
int CurrentIndex = 0 ;
bool InQuote = false ;
while ( CurrentIndex < FinalIndex )
{
char CurChar = Input [ CurrentIndex + + ] ;
if ( InQuote )
{
if ( CurChar = = '\\' )
{
if ( CurrentIndex < FinalIndex )
{
CurChar = Input [ CurrentIndex + + ] ;
WorkString . Append ( CurChar ) ;
}
}
else if ( CurChar = = '"' )
{
InQuote = false ;
}
else
{
WorkString . Append ( CurChar ) ;
}
}
else
{
if ( CurChar = = '"' )
{
InQuote = true ;
}
else if ( CurChar = = ',' )
{
Results . Add ( WorkString . ToString ( ) ) ;
WorkString . Clear ( ) ;
}
else if ( ! char . IsWhiteSpace ( CurChar ) )
{
WorkString . Append ( CurChar ) ;
}
}
}
if ( CurrentIndex > 0 )
{
Results . Add ( WorkString . ToString ( ) ) ;
}
return Results ;
}
static void ParseFilterFile ( string Filename )
{
if ( File . Exists ( Filename ) )
{
ActiveStdOutFilter = new List < FilterOperation > ( ) ;
string [ ] FilterContents = File . ReadAllLines ( Filename ) ;
foreach ( string FileLine in FilterContents )
{
List < string > Parts = ParseCSVString ( FileLine ) ;
if ( Parts . Count > 1 )
{
if ( Parts [ 0 ] . Equals ( "S" ) )
{
ActiveStdOutFilter . Add ( new FilterOperation ( FilterAction . Skip , Parts [ 1 ] , "" , "" ) ) ;
}
else if ( Parts [ 0 ] . Equals ( "R" ) )
{
if ( Parts . Count = = 4 )
{
ActiveStdOutFilter . Add ( new FilterOperation ( FilterAction . Replace , Parts [ 1 ] , Parts [ 2 ] , Parts [ 3 ] ) ) ;
}
}
else if ( Parts [ 0 ] . Equals ( "E" ) )
{
if ( Parts . Count = = 4 )
{
ActiveStdOutFilter . Add ( new FilterOperation ( FilterAction . Error , Parts [ 1 ] , Parts [ 2 ] , Parts [ 3 ] ) ) ;
}
}
}
}
if ( ActiveStdOutFilter . Count = = 0 )
{
ActiveStdOutFilter = null ;
}
}
}
2022-05-25 19:55:37 -04:00
static void FilterStdOutErr ( object sender , DataReceivedEventArgs e , ILogger Logger )
2022-05-24 19:41:41 -04:00
{
if ( e . Data ! = null )
{
if ( ActiveStdOutFilter ! = null )
{
foreach ( FilterOperation FilterOp in ActiveStdOutFilter )
{
if ( e . Data . Contains ( FilterOp . Condition ) )
{
switch ( FilterOp . Action )
{
case FilterAction . Skip :
break ;
case FilterAction . Replace :
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "{Output}" , e . Data . Replace ( FilterOp . Match , FilterOp . ReplaceWith ) ) ;
2022-05-24 19:41:41 -04:00
break ;
case FilterAction . Error :
2022-05-25 19:55:37 -04:00
Logger . LogError ( "{Output}" , e . Data . Replace ( FilterOp . Match , FilterOp . ReplaceWith ) ) ;
2022-05-24 19:41:41 -04:00
break ;
default :
break ;
}
return ;
}
}
}
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "{Output}" , e . Data ) ;
2022-05-24 19:41:41 -04:00
}
}
2022-05-25 19:55:37 -04:00
private static void RunCommandLineProgramWithExceptionAndFiltering ( string WorkingDirectory , string Command , string Params , ILogger Logger , string? OverrideDesc = null , bool bUseShellExecute = false )
2022-05-24 19:41:41 -04:00
{
// Process Arguments follow windows conventions in .NET Core
// Which means single quotes ' are not considered quotes.
// see https://github.com/dotnet/runtime/issues/29857
// also see UE-102580
// for rules see https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args
if ( Params . Contains ( "\'" ) )
{
Params = Params . Replace ( "\"" , "\\\"" ) ;
Params = Params . Replace ( '\'' , '\"' ) ;
}
if ( OverrideDesc = = null )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\nRunning: {Command} {Params}" , Command , Params ) ;
2022-05-24 19:41:41 -04:00
}
else if ( OverrideDesc ! = "" )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "{Message}" , OverrideDesc ) ;
Logger . LogDebug ( "\nRunning: {Command} {Params}" , Command , Params ) ;
2022-05-24 19:41:41 -04:00
}
ProcessStartInfo StartInfo = new ProcessStartInfo ( ) ;
StartInfo . WorkingDirectory = WorkingDirectory ;
StartInfo . FileName = Command ;
StartInfo . Arguments = Params ;
StartInfo . UseShellExecute = bUseShellExecute ;
StartInfo . WindowStyle = ProcessWindowStyle . Minimized ;
StartInfo . RedirectStandardInput = true ;
StartInfo . RedirectStandardOutput = true ;
StartInfo . RedirectStandardError = true ;
Process Proc = new Process ( ) ;
Proc . StartInfo = StartInfo ;
2022-05-25 19:55:37 -04:00
Proc . OutputDataReceived + = ( s , e ) = > FilterStdOutErr ( s , e , Logger ) ;
Proc . ErrorDataReceived + = ( s , e ) = > FilterStdOutErr ( s , e , Logger ) ;
2022-05-24 19:41:41 -04:00
Proc . Start ( ) ;
Proc . BeginOutputReadLine ( ) ;
Proc . BeginErrorReadLine ( ) ;
StreamWriter StreamIn = Proc . StandardInput ;
StreamIn . WriteLine ( "yes" ) ;
StreamIn . Close ( ) ;
Proc . WaitForExit ( ) ;
// android bat failure
if ( Proc . ExitCode ! = 0 )
{
throw new BuildException ( "{0} failed with args {1}" , Command , Params ) ;
}
}
private bool CheckApplicationName ( string UnrealBuildPath , string ProjectName , out string? ApplicationDisplayName )
{
string StringsXMLPath = Path . Combine ( UnrealBuildPath , "res/values/strings.xml" ) ;
ApplicationDisplayName = null ;
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "ApplicationDisplayName" , out ApplicationDisplayName ) ;
// use project name if display name is left blank
if ( String . IsNullOrWhiteSpace ( ApplicationDisplayName ) )
{
ApplicationDisplayName = ProjectName ;
}
// replace escaped characters (note: changes &# pattern before &, then patches back to allow escaped character codes in the string)
ApplicationDisplayName = ApplicationDisplayName . Replace ( "&#" , "$@#$" ) . Replace ( "&" , "&" ) . Replace ( "'" , "\\'" ) . Replace ( "\"" , "\\\"" ) . Replace ( "<" , "<" ) . Replace ( ">" , ">" ) . Replace ( "$@#$" , "&#" ) ;
// if it doesn't exist, need to repackage
if ( ! File . Exists ( StringsXMLPath ) )
{
return true ;
}
// read it and see if needs to be updated
string Contents = File . ReadAllText ( StringsXMLPath ) ;
// find the key
string AppNameTag = "<string name=\"app_name\">" ;
int KeyIndex = Contents . IndexOf ( AppNameTag ) ;
// if doesn't exist, need to repackage
if ( KeyIndex < 0 )
{
return true ;
}
// get the current value
KeyIndex + = AppNameTag . Length ;
int TagEnd = Contents . IndexOf ( "</string>" , KeyIndex ) ;
if ( TagEnd < 0 )
{
return true ;
}
string CurrentApplicationName = Contents . Substring ( KeyIndex , TagEnd - KeyIndex ) ;
// no need to do anything if matches
if ( CurrentApplicationName = = ApplicationDisplayName )
{
// name matches, no need to force a repackage
return false ;
}
// need to repackage
return true ;
}
private string GetAllBuildSettings ( AndroidToolChain ToolChain , UnrealPluginLanguage UPL , bool bForDistribution , bool bMakeSeparateApks , bool bPackageDataInsideApk , bool bDisableVerifyOBBOnStartUp , bool bUseExternalFilesDir , string TemplatesHashCode )
{
// make the settings string - this will be char by char compared against last time
StringBuilder CurrentSettings = new StringBuilder ( ) ;
CurrentSettings . AppendLine ( string . Format ( "NDKROOT={0}" , Environment . GetEnvironmentVariable ( "NDKROOT" ) ) ) ;
CurrentSettings . AppendLine ( string . Format ( "ANDROID_HOME={0}" , Environment . GetEnvironmentVariable ( "ANDROID_HOME" ) ) ) ;
CurrentSettings . AppendLine ( string . Format ( "JAVA_HOME={0}" , Environment . GetEnvironmentVariable ( "JAVA_HOME" ) ) ) ;
CurrentSettings . AppendLine ( string . Format ( "NDKVersion={0}" , ToolChain . GetNdkApiLevel ( ) ) ) ;
CurrentSettings . AppendLine ( string . Format ( "SDKVersion={0}" , GetSdkApiLevel ( ToolChain ) ) ) ;
CurrentSettings . AppendLine ( string . Format ( "bForDistribution={0}" , bForDistribution ) ) ;
CurrentSettings . AppendLine ( string . Format ( "bMakeSeparateApks={0}" , bMakeSeparateApks ) ) ;
CurrentSettings . AppendLine ( string . Format ( "bPackageDataInsideApk={0}" , bPackageDataInsideApk ) ) ;
CurrentSettings . AppendLine ( string . Format ( "bDisableVerifyOBBOnStartUp={0}" , bDisableVerifyOBBOnStartUp ) ) ;
CurrentSettings . AppendLine ( string . Format ( "bUseExternalFilesDir={0}" , bUseExternalFilesDir ) ) ;
CurrentSettings . AppendLine ( string . Format ( "UPLHashCode={0}" , UPLHashCode ) ) ;
CurrentSettings . AppendLine ( string . Format ( "TemplatesHashCode={0}" , TemplatesHashCode ) ) ;
// all AndroidRuntimeSettings ini settings in here
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
ConfigHierarchySection Section = Ini . FindSection ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" ) ;
if ( Section ! = null )
{
foreach ( string Key in Section . KeyNames )
{
// filter out NDK and SDK override since actual resolved versions already written above
if ( Key . Equals ( "SDKAPILevelOverride" ) | | Key . Equals ( "NDKAPILevelOverride" ) )
{
continue ;
}
if ( Section . TryGetValues ( Key , out IReadOnlyList < string > ? Values ) )
{
foreach ( string Value in Values )
{
CurrentSettings . AppendLine ( string . Format ( "{0}={1}" , Key , Value ) ) ;
}
}
}
}
Section = Ini . FindSection ( "/Script/AndroidPlatformEditor.AndroidSDKSettings" ) ;
if ( Section ! = null )
{
foreach ( string Key in Section . KeyNames )
{
// filter out NDK and SDK levels since actual resolved versions already written above
if ( Key . Equals ( "SDKAPILevel" ) | | Key . Equals ( "NDKAPILevel" ) )
{
continue ;
}
if ( Section . TryGetValues ( Key , out IReadOnlyList < string > ? Values ) )
{
foreach ( string Value in Values )
{
CurrentSettings . AppendLine ( string . Format ( "{0}={1}" , Key , Value ) ) ;
}
}
}
}
List < string > Arches = ToolChain . GetAllArchitectures ( ) ;
foreach ( string Arch in Arches )
{
CurrentSettings . AppendFormat ( "Arch={0}{1}" , Arch , Environment . NewLine ) ;
}
// Modifying some settings in the GameMapsSettings could trigger the OBB regeneration
// and make the cached OBBData.java mismatch to the actually data.
// So we insert the relevant keys into CurrentSettings to capture the change, to
// enforce the refreshing of Android java codes
Section = Ini . FindSection ( "/Script/EngineSettings.GameMapsSettings" ) ;
if ( Section ! = null )
{
foreach ( string Key in Section . KeyNames )
{
if ( ! Key . Equals ( "GameDefaultMap" ) & &
! Key . Equals ( "GlobalDefaultGameMode" ) )
{
continue ;
}
if ( Section . TryGetValues ( Key , out IReadOnlyList < string > ? Values ) )
{
foreach ( string Value in Values )
{
CurrentSettings . AppendLine ( string . Format ( "{0}={1}" , Key , Value ) ) ;
}
}
}
}
// get a list of the ini settings in UPL files that may affect the build
// architecture doesn't matter here since this node does not use init logic
string UPLBuildSettings = UPL . ProcessPluginNode ( "arm64-v8a" , "registerBuildSettings" , "" ) ;
foreach ( string Line in UPLBuildSettings . Split ( '\n' , StringSplitOptions . RemoveEmptyEntries ) )
{
string SectionName = Line . Trim ( ) ;
// needed keys are provided in [ ] separated by commas
string [ ] ? NeededKeys = null ;
int KeyIndex = SectionName . IndexOf ( '[' ) ;
if ( KeyIndex > 0 )
{
string KeyList = SectionName . Substring ( KeyIndex + 1 ) ;
SectionName = SectionName . Substring ( 0 , KeyIndex ) ;
int CloseIndex = KeyList . IndexOf ( "]" ) ;
if ( CloseIndex > 1 )
{
NeededKeys = KeyList . Substring ( 0 , CloseIndex ) . Split ( ',' , StringSplitOptions . RemoveEmptyEntries ) ;
if ( NeededKeys . Length = = 0 )
{
NeededKeys = null ;
}
}
}
// write the values for the requested keys (or all if none specified)
Section = Ini . FindSection ( SectionName ) ;
if ( Section ! = null )
{
foreach ( string Key in Section . KeyNames )
{
if ( NeededKeys ! = null & & ! NeededKeys . Contains ( Key ) )
{
continue ;
}
if ( Section . TryGetValues ( Key , out IReadOnlyList < string > ? Values ) )
{
foreach ( string Value in Values )
{
CurrentSettings . AppendLine ( string . Format ( "{0}:{1}={2}" , SectionName , Key , Value ) ) ;
}
}
}
}
}
return CurrentSettings . ToString ( ) ;
}
private bool CheckDependencies ( AndroidToolChain ToolChain , string ProjectName , string ProjectDirectory , string UnrealBuildFilesPath , string GameBuildFilesPath , string EngineDirectory , List < string > SettingsFiles ,
string CookFlavor , string OutputPath , bool bMakeSeparateApks , bool bPackageDataInsideApk )
{
List < string > Arches = ToolChain . GetAllArchitectures ( ) ;
// check all input files (.so, java files, .ini files, etc)
bool bAllInputsCurrent = true ;
foreach ( string Arch in Arches )
{
string SourceSOName = AndroidToolChain . InlineArchName ( OutputPath , Arch ) ;
// if the source binary was UnrealGame, replace it with the new project name, when re-packaging a binary only build
string ApkFilename = Path . GetFileNameWithoutExtension ( OutputPath ) . Replace ( "UnrealGame" , ProjectName ) ;
string DestApkName = Path . Combine ( ProjectDirectory , "Binaries/Android/" ) + ApkFilename + ".apk" ;
// if we making multiple Apks, we need to put the architecture into the name
if ( bMakeSeparateApks )
{
DestApkName = AndroidToolChain . InlineArchName ( DestApkName , Arch ) ;
}
// check to see if it's out of date before trying the slow make apk process (look at .so and all Engine and Project build files to be safe)
List < String > InputFiles = new List < string > ( ) ;
InputFiles . Add ( SourceSOName ) ;
InputFiles . AddRange ( Directory . EnumerateFiles ( UnrealBuildFilesPath , "*.*" , SearchOption . AllDirectories ) ) ;
if ( Directory . Exists ( GameBuildFilesPath ) )
{
InputFiles . AddRange ( Directory . EnumerateFiles ( GameBuildFilesPath , "*.*" , SearchOption . AllDirectories ) ) ;
}
// make sure changed java files will rebuild apk
InputFiles . AddRange ( SettingsFiles ) ;
// rebuild if .pak files exist for OBB in APK case
if ( bPackageDataInsideApk )
{
string PAKFileLocation = ProjectDirectory + "/Saved/StagedBuilds/Android" + CookFlavor + "/" + ProjectName + "/Content/Paks" ;
if ( Directory . Exists ( PAKFileLocation ) )
{
IEnumerable < string > PakFiles = Directory . EnumerateFiles ( PAKFileLocation , "*.pak" , SearchOption . TopDirectoryOnly ) ;
foreach ( string Name in PakFiles )
{
InputFiles . Add ( Name ) ;
}
}
}
// look for any newer input file
DateTime ApkTime = File . GetLastWriteTimeUtc ( DestApkName ) ;
foreach ( string InputFileName in InputFiles )
{
if ( File . Exists ( InputFileName ) )
{
// skip .log files
if ( Path . GetExtension ( InputFileName ) = = ".log" )
{
continue ;
}
DateTime InputFileTime = File . GetLastWriteTimeUtc ( InputFileName ) ;
if ( InputFileTime . CompareTo ( ApkTime ) > 0 )
{
bAllInputsCurrent = false ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "{DestApkName} is out of date due to newer input file {InputFileName}" , DestApkName , InputFileName ) ;
2022-05-24 19:41:41 -04:00
break ;
}
}
}
}
return bAllInputsCurrent ;
}
private int ConvertDepthBufferIniValue ( string IniValue )
{
switch ( IniValue . ToLower ( ) )
{
case "bits16" :
return 16 ;
case "bits24" :
return 24 ;
case "bits32" :
return 32 ;
default :
return 0 ;
}
}
private string ConvertOrientationIniValue ( string IniValue )
{
switch ( IniValue . ToLower ( ) )
{
case "portrait" :
return "portrait" ;
case "reverseportrait" :
return "reversePortrait" ;
case "sensorportrait" :
return "sensorPortrait" ;
case "landscape" :
return "landscape" ;
case "reverselandscape" :
return "reverseLandscape" ;
case "sensorlandscape" :
return "sensorLandscape" ;
case "sensor" :
return "sensor" ;
case "fullsensor" :
return "fullSensor" ;
default :
return "landscape" ;
}
}
private string GetOrientation ( string NDKArch )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
string Orientation ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "Orientation" , out Orientation ) ;
// check for UPL override
string OrientationOverride = UPL ! . ProcessPluginNode ( NDKArch , "orientationOverride" , "" ) ;
if ( ! String . IsNullOrEmpty ( OrientationOverride ) )
{
Orientation = OrientationOverride ;
}
return ConvertOrientationIniValue ( Orientation ) ;
}
private void DetermineScreenOrientationRequirements ( string Arch , out bool bNeedPortrait , out bool bNeedLandscape )
{
bNeedLandscape = false ;
bNeedPortrait = false ;
switch ( GetOrientation ( Arch ) . ToLower ( ) )
{
case "portrait" :
bNeedPortrait = true ;
break ;
case "reverseportrait" :
bNeedPortrait = true ;
break ;
case "sensorportrait" :
bNeedPortrait = true ;
break ;
case "landscape" :
bNeedLandscape = true ;
break ;
case "reverselandscape" :
bNeedLandscape = true ;
break ;
case "sensorlandscape" :
bNeedLandscape = true ;
break ;
case "sensor" :
bNeedPortrait = true ;
bNeedLandscape = true ;
break ;
case "fullsensor" :
bNeedPortrait = true ;
bNeedLandscape = true ;
break ;
default :
bNeedPortrait = true ;
bNeedLandscape = true ;
break ;
}
}
private void PickDownloaderScreenOrientation ( string UnrealBuildPath , bool bNeedPortrait , bool bNeedLandscape )
{
// Remove unused downloader_progress.xml to prevent missing resource
if ( ! bNeedPortrait )
{
string LayoutPath = UnrealBuildPath + "/res/layout-port/downloader_progress.xml" ;
SafeDeleteFile ( LayoutPath ) ;
}
if ( ! bNeedLandscape )
{
string LayoutPath = UnrealBuildPath + "/res/layout-land/downloader_progress.xml" ;
SafeDeleteFile ( LayoutPath ) ;
}
// Loop through each of the resolutions (only /res/drawable/ is required, others are optional)
string [ ] Resolutions = new string [ ] { "/res/drawable/" , "/res/drawable-ldpi/" , "/res/drawable-mdpi/" , "/res/drawable-hdpi/" , "/res/drawable-xhdpi/" } ;
foreach ( string ResolutionPath in Resolutions )
{
string PortraitFilename = UnrealBuildPath + ResolutionPath + "downloadimagev.png" ;
if ( bNeedPortrait )
{
if ( ! File . Exists ( PortraitFilename ) & & ( ResolutionPath = = "/res/drawable/" ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "Warning: Downloader screen source image {PortraitFilename} not available, downloader screen will not function properly!" , PortraitFilename ) ;
2022-05-24 19:41:41 -04:00
}
}
else
{
// Remove unused image
SafeDeleteFile ( PortraitFilename ) ;
}
string LandscapeFilename = UnrealBuildPath + ResolutionPath + "downloadimageh.png" ;
if ( bNeedLandscape )
{
if ( ! File . Exists ( LandscapeFilename ) & & ( ResolutionPath = = "/res/drawable/" ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "Warning: Downloader screen source image {LandscapeFilename} not available, downloader screen will not function properly!" , LandscapeFilename ) ;
2022-05-24 19:41:41 -04:00
}
}
else
{
// Remove unused image
SafeDeleteFile ( LandscapeFilename ) ;
}
}
}
private void PackageForDaydream ( string UnrealBuildPath )
{
bool bPackageForDaydream = IsPackagingForDaydream ( ) ;
if ( ! bPackageForDaydream )
{
// If this isn't a Daydream App, we need to make sure to remove
// Daydream specific assets.
// Remove the Daydream app tile background.
string AppTileBackgroundPath = UnrealBuildPath + "/res/drawable-nodpi/vr_icon_background.png" ;
SafeDeleteFile ( AppTileBackgroundPath ) ;
// Remove the Daydream app tile icon.
string AppTileIconPath = UnrealBuildPath + "/res/drawable-nodpi/vr_icon.png" ;
SafeDeleteFile ( AppTileIconPath ) ;
}
}
private void PickSplashScreenOrientation ( string UnrealBuildPath , bool bNeedPortrait , bool bNeedLandscape )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
bool bShowLaunchImage = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bShowLaunchImage" , out bShowLaunchImage ) ;
bool bPackageForOculusMobile = IsPackagingForOculusMobile ( Ini ) ; ;
bool bPackageForDaydream = IsPackagingForDaydream ( Ini ) ;
//override the parameters if we are not showing a launch image or are packaging for Oculus Mobile and Daydream
if ( bPackageForOculusMobile | | bPackageForDaydream | | ! bShowLaunchImage )
{
bNeedPortrait = bNeedLandscape = false ;
}
// Remove unused styles.xml to prevent missing resource
if ( ! bNeedPortrait )
{
string StylesPath = UnrealBuildPath + "/res/values-port/styles.xml" ;
SafeDeleteFile ( StylesPath ) ;
}
if ( ! bNeedLandscape )
{
string StylesPath = UnrealBuildPath + "/res/values-land/styles.xml" ;
SafeDeleteFile ( StylesPath ) ;
}
// Loop through each of the resolutions (only /res/drawable/ is required, others are optional)
string [ ] Resolutions = new string [ ] { "/res/drawable/" , "/res/drawable-ldpi/" , "/res/drawable-mdpi/" , "/res/drawable-hdpi/" , "/res/drawable-xhdpi/" } ;
foreach ( string ResolutionPath in Resolutions )
{
string PortraitFilename = UnrealBuildPath + ResolutionPath + "splashscreen_portrait.png" ;
if ( bNeedPortrait )
{
if ( ! File . Exists ( PortraitFilename ) & & ( ResolutionPath = = "/res/drawable/" ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "Warning: Splash screen source image {PortraitFilename} not available, splash screen will not function properly!" , PortraitFilename ) ;
2022-05-24 19:41:41 -04:00
}
}
else
{
// Remove unused image
SafeDeleteFile ( PortraitFilename ) ;
// Remove optional extended resource
string PortraitXmlFilename = UnrealBuildPath + ResolutionPath + "splashscreen_p.xml" ;
SafeDeleteFile ( PortraitXmlFilename ) ;
}
string LandscapeFilename = UnrealBuildPath + ResolutionPath + "splashscreen_landscape.png" ;
if ( bNeedLandscape )
{
if ( ! File . Exists ( LandscapeFilename ) & & ( ResolutionPath = = "/res/drawable/" ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "Warning: Splash screen source image {LandscapeFilename} not available, splash screen will not function properly!" , LandscapeFilename ) ;
2022-05-24 19:41:41 -04:00
}
}
else
{
// Remove unused image
SafeDeleteFile ( LandscapeFilename ) ;
// Remove optional extended resource
string LandscapeXmlFilename = UnrealBuildPath + ResolutionPath + "splashscreen_l.xml" ;
SafeDeleteFile ( LandscapeXmlFilename ) ;
}
}
}
private string? CachedPackageName = null ;
private bool IsLetter ( char Input )
{
return ( Input > = 'A' & & Input < = 'Z' ) | | ( Input > = 'a' & & Input < = 'z' ) ;
}
private bool IsDigit ( char Input )
{
return ( Input > = '0' & & Input < = '9' ) ;
}
private string GetPackageName ( string ProjectName )
{
if ( CachedPackageName = = null )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
string PackageName ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "PackageName" , out PackageName ) ;
if ( PackageName . Contains ( "[PROJECT]" ) )
{
// project name must start with a letter
if ( ! IsLetter ( ProjectName [ 0 ] ) )
{
throw new BuildException ( "Package name segments must all start with a letter. Please replace [PROJECT] with a valid name" ) ;
}
// hyphens not allowed so change them to underscores in project name
if ( ProjectName . Contains ( "-" ) )
{
Trace . TraceWarning ( "Project name contained hyphens, converted to underscore" ) ;
ProjectName = ProjectName . Replace ( "-" , "_" ) ;
}
// check for special characters
for ( int Index = 0 ; Index < ProjectName . Length ; Index + + )
{
char c = ProjectName [ Index ] ;
if ( c ! = '.' & & c ! = '_' & & ! IsDigit ( c ) & & ! IsLetter ( c ) )
{
throw new BuildException ( "Project name contains illegal characters (only letters, numbers, and underscore allowed); please replace [PROJECT] with a valid name" ) ;
}
}
PackageName = PackageName . Replace ( "[PROJECT]" , ProjectName ) ;
}
// verify minimum number of segments
string [ ] PackageParts = PackageName . Split ( '.' ) ;
int SectionCount = PackageParts . Length ;
if ( SectionCount < 2 )
{
throw new BuildException ( "Package name must have at least 2 segments separated by periods (ex. com.projectname, not projectname); please change in Android Project Settings. Currently set to '" + PackageName + "'" ) ;
}
// hyphens not allowed
if ( PackageName . Contains ( "-" ) )
{
throw new BuildException ( "Package names may not contain hyphens; please change in Android Project Settings. Currently set to '" + PackageName + "'" ) ;
}
// do not allow special characters
for ( int Index = 0 ; Index < PackageName . Length ; Index + + )
{
char c = PackageName [ Index ] ;
if ( c ! = '.' & & c ! = '_' & & ! IsDigit ( c ) & & ! IsLetter ( c ) )
{
throw new BuildException ( "Package name contains illegal characters (only letters, numbers, and underscore allowed); please change in Android Project Settings. Currently set to '" + PackageName + "'" ) ;
}
}
// validate each segment
for ( int Index = 0 ; Index < SectionCount ; Index + + )
{
if ( PackageParts [ Index ] . Length < 1 )
{
throw new BuildException ( "Package name segments must have at least one letter; please change in Android Project Settings. Currently set to '" + PackageName + "'" ) ;
}
if ( ! IsLetter ( PackageParts [ Index ] [ 0 ] ) )
{
throw new BuildException ( "Package name segments must start with a letter; please change in Android Project Settings. Currently set to '" + PackageName + "'" ) ;
}
// cannot use Java reserved keywords
foreach ( string Keyword in JavaReservedKeywords )
{
if ( PackageParts [ Index ] = = Keyword )
{
throw new BuildException ( "Package name segments must not be a Java reserved keyword (" + Keyword + "); please change in Android Project Settings. Currently set to '" + PackageName + "'" ) ;
}
}
}
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Using package name: '{PackageName}'" , PackageName ) ;
2022-05-24 19:41:41 -04:00
CachedPackageName = PackageName ;
}
return CachedPackageName ;
}
private string GetPublicKey ( )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
string PlayLicenseKey = "" ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "GooglePlayLicenseKey" , out PlayLicenseKey ) ;
return PlayLicenseKey ;
}
private bool bHaveReadEngineVersion = false ;
private string EngineMajorVersion = "4" ;
private string EngineMinorVersion = "0" ;
private string EnginePatchVersion = "0" ;
private string EngineChangelist = "0" ;
private string? EngineBranch = "UE5" ;
private string ReadEngineVersion ( )
{
if ( ! bHaveReadEngineVersion )
{
ReadOnlyBuildVersion Version = ReadOnlyBuildVersion . Current ;
EngineMajorVersion = Version . MajorVersion . ToString ( ) ;
EngineMinorVersion = Version . MinorVersion . ToString ( ) ;
EnginePatchVersion = Version . PatchVersion . ToString ( ) ;
EngineChangelist = Version . Changelist . ToString ( ) ;
EngineBranch = Version . BranchName ;
bHaveReadEngineVersion = true ;
}
return EngineMajorVersion + "." + EngineMinorVersion + "." + EnginePatchVersion ;
}
private string GenerateManifest ( AndroidToolChain ToolChain , string ProjectName , TargetType InTargetType , string EngineDirectory , bool bIsForDistribution , bool bPackageDataInsideApk , string GameBuildFilesPath , bool bHasOBBFiles , bool bDisableVerifyOBBOnStartUp , string UnrealArch , string CookFlavor , bool bUseExternalFilesDir , string Configuration , int SDKLevelInt , bool bIsEmbedded , bool bEnableBundle )
{
// Read the engine version
string EngineVersion = ReadEngineVersion ( ) ;
int StoreVersion = GetStoreVersion ( UnrealArch ) ;
string Arch = GetNDKArch ( UnrealArch ) ;
int NDKLevelInt = 0 ;
int MinSDKVersion = 0 ;
int TargetSDKVersion = 0 ;
GetMinTargetSDKVersions ( ToolChain , UnrealArch , UPL ! , Arch , bEnableBundle , out MinSDKVersion , out TargetSDKVersion , out NDKLevelInt ) ;
// get project version from ini
ConfigHierarchy GameIni = GetConfigCacheIni ( ConfigHierarchyType . Game ) ;
string ProjectVersion ;
GameIni . GetString ( "/Script/EngineSettings.GeneralProjectSettings" , "ProjectVersion" , out ProjectVersion ) ;
// ini file to get settings from
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
string PackageName = GetPackageName ( ProjectName ) ;
string VersionDisplayName = GetVersionDisplayName ( bIsEmbedded ) ;
bool bEnableGooglePlaySupport ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bEnableGooglePlaySupport" , out bEnableGooglePlaySupport ) ;
bool bUseGetAccounts ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bUseGetAccounts" , out bUseGetAccounts ) ;
string DepthBufferPreference ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "DepthBufferPreference" , out DepthBufferPreference ) ;
float MaxAspectRatioValue ;
if ( ! Ini . TryGetValue ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "MaxAspectRatio" , out MaxAspectRatioValue ) )
{
MaxAspectRatioValue = 2.1f ;
}
string Orientation = ConvertOrientationIniValue ( GetOrientation ( Arch ) ) ;
bool EnableFullScreen ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bFullScreen" , out EnableFullScreen ) ;
bool bUseDisplayCutout ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bUseDisplayCutout" , out bUseDisplayCutout ) ;
bool bRestoreNotificationsOnReboot = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bRestoreNotificationsOnReboot" , out bRestoreNotificationsOnReboot ) ;
List < string > ? ExtraManifestNodeTags ;
Ini . GetArray ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "ExtraManifestNodeTags" , out ExtraManifestNodeTags ) ;
List < string > ? ExtraApplicationNodeTags ;
Ini . GetArray ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "ExtraApplicationNodeTags" , out ExtraApplicationNodeTags ) ;
List < string > ? ExtraActivityNodeTags ;
Ini . GetArray ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "ExtraActivityNodeTags" , out ExtraActivityNodeTags ) ;
string ExtraActivitySettings ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "ExtraActivitySettings" , out ExtraActivitySettings ) ;
string ExtraApplicationSettings ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "ExtraApplicationSettings" , out ExtraApplicationSettings ) ;
List < string > ? ExtraPermissions ;
Ini . GetArray ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "ExtraPermissions" , out ExtraPermissions ) ;
bool bPackageForOculusMobile = IsPackagingForOculusMobile ( Ini ) ;
bool bEnableIAP = false ;
Ini . GetBool ( "OnlineSubsystemGooglePlay.Store" , "bSupportsInAppPurchasing" , out bEnableIAP ) ;
bool bShowLaunchImage = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bShowLaunchImage" , out bShowLaunchImage ) ;
string AndroidGraphicsDebugger ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "AndroidGraphicsDebugger" , out AndroidGraphicsDebugger ) ;
bool bSupportAdMob = true ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bSupportAdMob" , out bSupportAdMob ) ;
bool bValidateTextureFormats ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bValidateTextureFormats" , out bValidateTextureFormats ) ;
bool bBuildForES31 = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bBuildForES31" , out bBuildForES31 ) ;
bool bSupportsVulkan = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bSupportsVulkan" , out bSupportsVulkan ) ;
int PropagateAlpha = 0 ;
Ini . GetInt32 ( "/Script/Engine.RendererSettings" , "r.Mobile.PropagateAlpha" , out PropagateAlpha ) ;
bool bAllowIMU = true ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bAllowIMU" , out bAllowIMU ) ;
if ( IsPackagingForDaydream ( Ini ) & & bAllowIMU )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Daydream and IMU both enabled, recommend disabling IMU if not needed." ) ;
2022-05-24 19:41:41 -04:00
}
bool bExtractNativeLibs = true ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bExtractNativeLibs" , out bExtractNativeLibs ) ;
bool bPublicLogFiles = true ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bPublicLogFiles" , out bPublicLogFiles ) ;
if ( ! bUseExternalFilesDir )
{
bPublicLogFiles = false ;
}
string InstallLocation ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "InstallLocation" , out InstallLocation ) ;
switch ( InstallLocation . ToLower ( ) )
{
case "preferexternal" :
InstallLocation = "preferExternal" ;
break ;
case "auto" :
InstallLocation = "auto" ;
break ;
default :
InstallLocation = "internalOnly" ;
break ;
}
// only apply density to configChanges if using android-24 or higher and minimum sdk is 17
bool bAddDensity = ( SDKLevelInt > = 24 ) & & ( MinSDKVersion > = 17 ) ;
// disable Oculus Mobile if not supported platform (in this case only armv7 for now)
if ( UnrealArch ! = "-armv7" & & UnrealArch ! = "-arm64" )
{
if ( bPackageForOculusMobile )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Disabling Package For Oculus Mobile for unsupported architecture {UnrealArch}" , UnrealArch ) ;
2022-05-24 19:41:41 -04:00
bPackageForOculusMobile = false ;
}
}
// disable splash screen for Oculus Mobile (for now)
if ( bPackageForOculusMobile )
{
if ( bShowLaunchImage )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Disabling Show Launch Image for Oculus Mobile enabled application" ) ;
2022-05-24 19:41:41 -04:00
bShowLaunchImage = false ;
}
}
bool bPackageForDaydream = IsPackagingForDaydream ( Ini ) ;
// disable splash screen for daydream
if ( bPackageForDaydream )
{
if ( bShowLaunchImage )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Disabling Show Launch Image for Daydream enabled application" ) ;
2022-05-24 19:41:41 -04:00
bShowLaunchImage = false ;
}
}
//figure out the app type
string AppType = InTargetType = = TargetType . Game ? "" : InTargetType . ToString ( ) ;
if ( CookFlavor . EndsWith ( "Client" ) )
{
CookFlavor = CookFlavor . Substring ( 0 , CookFlavor . Length - 6 ) ;
}
if ( CookFlavor . EndsWith ( "Server" ) )
{
CookFlavor = CookFlavor . Substring ( 0 , CookFlavor . Length - 6 ) ;
}
//figure out which texture compressions are supported
bool bETC2Enabled , bDXTEnabled , bASTCEnabled ;
bETC2Enabled = bDXTEnabled = bASTCEnabled = false ;
if ( CookFlavor . Length < 1 )
{
//All values supported
bETC2Enabled = bDXTEnabled = bASTCEnabled = true ;
}
else
{
switch ( CookFlavor )
{
case "_Multi" :
//need to check ini to determine which are supported
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bMultiTargetFormat_ETC2" , out bETC2Enabled ) ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bMultiTargetFormat_DXT" , out bDXTEnabled ) ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bMultiTargetFormat_ASTC" , out bASTCEnabled ) ;
break ;
case "_ETC2" :
bETC2Enabled = true ;
break ;
case "_DXT" :
bDXTEnabled = true ;
break ;
case "_ASTC" :
bASTCEnabled = true ;
break ;
default :
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "Invalid or unknown CookFlavor used in GenerateManifest: {CookFlavor}" , CookFlavor ) ;
2022-05-24 19:41:41 -04:00
break ;
}
}
bool bSupportingAllTextureFormats = bETC2Enabled & & bDXTEnabled & & bASTCEnabled ;
// If it is only ETC2 we need to skip adding the texture format filtering and instead use ES 3.0 as minimum version (it requires ETC2)
bool bOnlyETC2Enabled = ( bETC2Enabled & & ! ( bDXTEnabled | | bASTCEnabled ) ) ;
string CookedFlavors = ( bETC2Enabled ? "ETC2," : "" ) +
( bDXTEnabled ? "DXT," : "" ) +
( bASTCEnabled ? "ASTC," : "" ) ;
CookedFlavors = ( CookedFlavors = = "" ) ? "" : CookedFlavors . Substring ( 0 , CookedFlavors . Length - 1 ) ;
StringBuilder Text = new StringBuilder ( ) ;
Text . AppendLine ( XML_HEADER ) ;
Text . AppendLine ( "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"" ) ;
Text . AppendLine ( string . Format ( " package=\"{0}\"" , PackageName ) ) ;
if ( ExtraManifestNodeTags ! = null )
{
foreach ( string Line in ExtraManifestNodeTags )
{
Text . AppendLine ( " " + Line ) ;
}
}
Text . AppendLine ( string . Format ( " android:installLocation=\"{0}\"" , InstallLocation ) ) ;
Text . AppendLine ( string . Format ( " android:versionCode=\"{0}\"" , StoreVersion ) ) ;
Text . AppendLine ( string . Format ( " android:versionName=\"{0}\">" , VersionDisplayName ) ) ;
Text . AppendLine ( "" ) ;
if ( TargetSDKVersion > = 30 )
{
Text . AppendLine ( "\t<queries>" ) ;
Text . AppendLine ( "\t\t<intent>" ) ;
Text . AppendLine ( "\t\t\t<action android:name=\"android.intent.action.VIEW\" />" ) ;
Text . AppendLine ( "\t\t\t<category android:name=\"android.intent.category.BROWSABLE\" />" ) ;
Text . AppendLine ( "\t\t\t<data android:scheme=\"http\" />" ) ;
Text . AppendLine ( "\t\t</intent>" ) ;
Text . AppendLine ( "\t\t<intent>" ) ;
Text . AppendLine ( "\t\t\t<action android:name=\"android.intent.action.VIEW\" />" ) ;
Text . AppendLine ( "\t\t\t<category android:name=\"android.intent.category.BROWSABLE\" />" ) ;
Text . AppendLine ( "\t\t\t<data android:scheme=\"https\" />" ) ;
Text . AppendLine ( "\t\t</intent>" ) ;
Text . AppendLine ( "\t</queries>" ) ;
}
Text . AppendLine ( "\t<!-- Application Definition -->" ) ;
Text . AppendLine ( "\t<application android:label=\"@string/app_name\"" ) ;
Text . AppendLine ( "\t android:icon=\"@drawable/icon\"" ) ;
AndroidToolChain . ClangSanitizer Sanitizer = ToolChain . BuildWithSanitizer ( ) ;
if ( ( Sanitizer ! = AndroidToolChain . ClangSanitizer . None & & Sanitizer ! = AndroidToolChain . ClangSanitizer . HwAddress ) )
{
bExtractNativeLibs = true ;
}
bool bRequestedLegacyExternalStorage = false ;
if ( ExtraApplicationNodeTags ! = null )
{
foreach ( string Line in ExtraApplicationNodeTags )
{
if ( Line . Contains ( "requestLegacyExternalStorage" ) )
{
bRequestedLegacyExternalStorage = true ;
}
Text . AppendLine ( "\t " + Line ) ;
}
}
Text . AppendLine ( "\t android:hardwareAccelerated=\"true\"" ) ;
Text . AppendLine ( string . Format ( "\t android:extractNativeLibs=\"{0}\"" , bExtractNativeLibs ? "true" : "false" ) ) ;
Text . AppendLine ( "\t android:name=\"com.epicgames.unreal.GameApplication\"" ) ;
if ( ! bIsForDistribution & & SDKLevelInt > = 29 & & ! bRequestedLegacyExternalStorage )
{
// work around scoped storage for non-distribution for SDK 29; add to ExtraApplicationNodeTags if you need it for distribution
Text . AppendLine ( "\t android:requestLegacyExternalStorage=\"true\"" ) ;
}
Text . AppendLine ( "\t android:hasCode=\"true\">" ) ;
if ( bShowLaunchImage )
{
// normal application settings
Text . AppendLine ( "\t\t<activity android:name=\"com.epicgames.unreal.SplashActivity\"" ) ;
Text . AppendLine ( "\t\t android:exported=\"true\"" ) ;
Text . AppendLine ( "\t\t android:label=\"@string/app_name\"" ) ;
Text . AppendLine ( "\t\t android:theme=\"@style/UnrealSplashTheme\"" ) ;
Text . AppendLine ( "\t\t android:launchMode=\"singleTask\"" ) ;
if ( SDKLevelInt > = 24 )
{
Text . AppendLine ( "\t\t android:resizeableActivity=\"false\"" ) ;
}
Text . AppendLine ( string . Format ( "\t\t android:screenOrientation=\"{0}\"" , Orientation ) ) ;
Text . AppendLine ( string . Format ( "\t\t android:debuggable=\"{0}\">" , bIsForDistribution ? "false" : "true" ) ) ;
Text . AppendLine ( "\t\t\t<intent-filter>" ) ;
Text . AppendLine ( "\t\t\t\t<action android:name=\"android.intent.action.MAIN\" />" ) ;
Text . AppendLine ( string . Format ( "\t\t\t\t<category android:name=\"android.intent.category.LAUNCHER\" />" ) ) ;
Text . AppendLine ( "\t\t\t</intent-filter>" ) ;
Text . AppendLine ( "\t\t</activity>" ) ;
Text . AppendLine ( "\t\t<activity android:name=\"com.epicgames.unreal.GameActivity\"" ) ;
Text . AppendLine ( "\t\t android:label=\"@string/app_name\"" ) ;
Text . AppendLine ( "\t\t android:theme=\"@style/UnrealSplashTheme\"" ) ;
Text . AppendLine ( bAddDensity ? "\t\t android:configChanges=\"mcc|mnc|uiMode|density|screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|touchscreen|locale|fontScale|layoutDirection\""
: "\t\t android:configChanges=\"mcc|mnc|uiMode|screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|touchscreen|locale|fontScale|layoutDirection\"" ) ;
}
else
{
Text . AppendLine ( "\t\t<activity android:name=\"com.epicgames.unreal.GameActivity\"" ) ;
Text . AppendLine ( "\t\t android:exported=\"true\"" ) ;
Text . AppendLine ( "\t\t android:label=\"@string/app_name\"" ) ;
Text . AppendLine ( "\t\t android:theme=\"@android:style/Theme.Black.NoTitleBar.Fullscreen\"" ) ;
Text . AppendLine ( bAddDensity ? "\t\t android:configChanges=\"mcc|mnc|uiMode|density|screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|touchscreen|locale|fontScale|layoutDirection\""
: "\t\t android:configChanges=\"mcc|mnc|uiMode|screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|touchscreen|locale|fontScale|layoutDirection\"" ) ;
}
if ( SDKLevelInt > = 24 )
{
Text . AppendLine ( "\t\t android:resizeableActivity=\"false\"" ) ;
}
Text . AppendLine ( "\t\t android:launchMode=\"singleTask\"" ) ;
Text . AppendLine ( string . Format ( "\t\t android:screenOrientation=\"{0}\"" , Orientation ) ) ;
if ( ExtraActivityNodeTags ! = null )
{
foreach ( string Line in ExtraActivityNodeTags )
{
Text . AppendLine ( "\t\t " + Line ) ;
}
}
Text . AppendLine ( string . Format ( "\t\t android:debuggable=\"{0}\">" , bIsForDistribution ? "false" : "true" ) ) ;
Text . AppendLine ( "\t\t\t<meta-data android:name=\"android.app.lib_name\" android:value=\"Unreal\"/>" ) ;
if ( ! bShowLaunchImage )
{
Text . AppendLine ( "\t\t\t<intent-filter>" ) ;
Text . AppendLine ( "\t\t\t\t<action android:name=\"android.intent.action.MAIN\" />" ) ;
Text . AppendLine ( string . Format ( "\t\t\t\t<category android:name=\"android.intent.category.LAUNCHER\" />" ) ) ;
Text . AppendLine ( "\t\t\t</intent-filter>" ) ;
}
if ( ! string . IsNullOrEmpty ( ExtraActivitySettings ) )
{
ExtraActivitySettings = ExtraActivitySettings . Replace ( "\\n" , "\n" ) ;
foreach ( string Line in ExtraActivitySettings . Split ( "\r\n" . ToCharArray ( ) ) )
{
Text . AppendLine ( "\t\t\t" + Line ) ;
}
}
string ActivityAdditionsFile = Path . Combine ( GameBuildFilesPath , "ManifestActivityAdditions.txt" ) ;
if ( File . Exists ( ActivityAdditionsFile ) )
{
foreach ( string Line in File . ReadAllLines ( ActivityAdditionsFile ) )
{
Text . AppendLine ( "\t\t\t" + Line ) ;
}
}
Text . AppendLine ( "\t\t</activity>" ) ;
// For OBB download support
if ( bShowLaunchImage )
{
Text . AppendLine ( "\t\t<activity android:name=\".DownloaderActivity\"" ) ;
Text . AppendLine ( string . Format ( "\t\t android:screenOrientation=\"{0}\"" , Orientation ) ) ;
Text . AppendLine ( bAddDensity ? "\t\t android:configChanges=\"mcc|mnc|uiMode|density|screenSize|orientation|keyboardHidden|keyboard\""
: "\t\t android:configChanges=\"mcc|mnc|uiMode|screenSize|orientation|keyboardHidden|keyboard\"" ) ;
Text . AppendLine ( "\t\t android:theme=\"@style/UnrealSplashTheme\" />" ) ;
}
else
{
Text . AppendLine ( "\t\t<activity android:name=\".DownloaderActivity\" />" ) ;
}
// Figure out the required startup permissions if targetting devices supporting runtime permissions
String StartupPermissions = "" ;
if ( TargetSDKVersion > = 23 )
{
if ( Configuration ! = "Shipping" | | ! bUseExternalFilesDir )
{
StartupPermissions = StartupPermissions + ( StartupPermissions . Length > 0 ? "," : "" ) + "android.permission.WRITE_EXTERNAL_STORAGE" ;
}
if ( bEnableGooglePlaySupport & & bUseGetAccounts )
{
StartupPermissions = StartupPermissions + ( StartupPermissions . Length > 0 ? "," : "" ) + "android.permission.GET_ACCOUNTS" ;
}
}
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.EngineVersion\" android:value=\"{0}\"/>" , EngineVersion ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.EngineBranch\" android:value=\"{0}\"/>" , EngineBranch ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.ProjectVersion\" android:value=\"{0}\"/>" , ProjectVersion ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.DepthBufferPreference\" android:value=\"{0}\"/>" , ConvertDepthBufferIniValue ( DepthBufferPreference ) ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bPackageDataInsideApk\" android:value=\"{0}\"/>" , bPackageDataInsideApk ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bVerifyOBBOnStartUp\" android:value=\"{0}\"/>" , ( bIsForDistribution & & ! bDisableVerifyOBBOnStartUp ) ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bShouldHideUI\" android:value=\"{0}\"/>" , EnableFullScreen ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.ProjectName\" android:value=\"{0}\"/>" , ProjectName ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.AppType\" android:value=\"{0}\"/>" , AppType ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bHasOBBFiles\" android:value=\"{0}\"/>" , bHasOBBFiles ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.BuildConfiguration\" android:value=\"{0}\"/>" , Configuration ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.CookedFlavors\" android:value=\"{0}\"/>" , CookedFlavors ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bValidateTextureFormats\" android:value=\"{0}\"/>" , bValidateTextureFormats ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bUseExternalFilesDir\" android:value=\"{0}\"/>" , bUseExternalFilesDir ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bPublicLogFiles\" android:value=\"{0}\"/>" , bPublicLogFiles ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bUseDisplayCutout\" android:value=\"{0}\"/>" , bUseDisplayCutout ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bAllowIMU\" android:value=\"{0}\"/>" , bAllowIMU ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bSupportsVulkan\" android:value=\"{0}\"/>" , bSupportsVulkan ? "true" : "false" ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.PropagateAlpha\" android:value=\"{0}\"/>" , PropagateAlpha ) ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.StartupPermissions\" android:value=\"{0}\"/>" , StartupPermissions ) ) ;
if ( bPackageForDaydream )
{
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"com.epicgames.unreal.GameActivity.bDaydream\" android:value=\"true\"/>" ) ) ;
}
Text . AppendLine ( "\t\t<meta-data android:name=\"com.google.android.gms.games.APP_ID\"" ) ;
Text . AppendLine ( "\t\t android:value=\"@string/app_id\" />" ) ;
Text . AppendLine ( "\t\t<meta-data android:name=\"com.google.android.gms.version\"" ) ;
Text . AppendLine ( "\t\t android:value=\"@integer/google_play_services_version\" />" ) ;
if ( bSupportAdMob )
{
Text . AppendLine ( "\t\t<activity android:name=\"com.google.android.gms.ads.AdActivity\"" ) ;
Text . AppendLine ( "\t\t android:configChanges=\"keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize\"/>" ) ;
}
if ( ! string . IsNullOrEmpty ( ExtraApplicationSettings ) )
{
ExtraApplicationSettings = ExtraApplicationSettings . Replace ( "\\n" , "\n" ) ;
foreach ( string Line in ExtraApplicationSettings . Split ( "\r\n" . ToCharArray ( ) ) )
{
Text . AppendLine ( "\t\t" + Line ) ;
}
}
string ApplicationAdditionsFile = Path . Combine ( GameBuildFilesPath , "ManifestApplicationAdditions.txt" ) ;
if ( File . Exists ( ApplicationAdditionsFile ) )
{
foreach ( string Line in File . ReadAllLines ( ApplicationAdditionsFile ) )
{
Text . AppendLine ( "\t\t" + Line ) ;
}
}
// Declare the 8 OpenGL program compiling services.
Text . AppendLine ( @" <service android:name=""com.epicgames.unreal.oglservices.OGLProgramService"" android:process="":oglprogramservice"" />" ) ;
// Declare the remaining 7 OpenGL program compiling services. (all derived from OGLProgramService)
for ( int i = 1 ; i < 8 ; i + + )
{
String serviceLine = String . Format ( " <service android:name=\"com.epicgames.unreal.oglservices.OGLProgramService{0}\" android:process=\":oglprogramservice{0}\" />" , i ) ;
Text . AppendLine ( serviceLine ) ;
}
// Required for OBB download support
Text . AppendLine ( "\t\t<service android:name=\"OBBDownloaderService\" />" ) ;
Text . AppendLine ( "\t\t<receiver android:name=\"AlarmReceiver\" />" ) ;
Text . AppendLine ( "\t\t<receiver android:name=\"com.epicgames.unreal.LocalNotificationReceiver\" />" ) ;
if ( bRestoreNotificationsOnReboot )
{
Text . AppendLine ( "\t\t<receiver android:name=\"com.epicgames.unreal.BootCompleteReceiver\" android:exported=\"true\">" ) ;
Text . AppendLine ( "\t\t\t<intent-filter>" ) ;
Text . AppendLine ( "\t\t\t\t<action android:name=\"android.intent.action.BOOT_COMPLETED\" />" ) ;
Text . AppendLine ( "\t\t\t\t<action android:name=\"android.intent.action.QUICKBOOT_POWERON\" />" ) ;
Text . AppendLine ( "\t\t\t\t<action android:name=\"com.htc.intent.action.QUICKBOOT_POWERON\" />" ) ;
Text . AppendLine ( "\t\t\t</intent-filter>" ) ;
Text . AppendLine ( "\t\t</receiver>" ) ;
}
Text . AppendLine ( "\t\t<receiver android:name=\"com.epicgames.unreal.MulticastBroadcastReceiver\" android:exported=\"true\">" ) ;
Text . AppendLine ( "\t\t\t<intent-filter>" ) ;
Text . AppendLine ( "\t\t\t\t<action android:name=\"com.android.vending.INSTALL_REFERRER\" />" ) ;
Text . AppendLine ( "\t\t\t</intent-filter>" ) ;
Text . AppendLine ( "\t\t</receiver>" ) ;
// Max supported aspect ratio
string MaxAspectRatioString = MaxAspectRatioValue . ToString ( "f" , System . Globalization . CultureInfo . InvariantCulture ) ;
Text . AppendLine ( string . Format ( "\t\t<meta-data android:name=\"android.max_aspect\" android:value=\"{0}\" />" , MaxAspectRatioString ) ) ;
Text . AppendLine ( "\t</application>" ) ;
Text . AppendLine ( "" ) ;
Text . AppendLine ( "\t<!-- Requirements -->" ) ;
// check for an override for the requirements section of the manifest
string RequirementsOverrideFile = Path . Combine ( GameBuildFilesPath , "ManifestRequirementsOverride.txt" ) ;
if ( File . Exists ( RequirementsOverrideFile ) )
{
foreach ( string Line in File . ReadAllLines ( RequirementsOverrideFile ) )
{
Text . AppendLine ( "\t" + Line ) ;
}
}
else
{
Text . AppendLine ( "\t<uses-feature android:glEsVersion=\"" + AndroidToolChain . GetGLESVersion ( bBuildForES31 ) + "\" android:required=\"true\" />" ) ;
Text . AppendLine ( "\t<uses-permission android:name=\"android.permission.INTERNET\"/>" ) ;
if ( Configuration ! = "Shipping" | | ! bUseExternalFilesDir )
{
Text . AppendLine ( "\t<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>" ) ;
}
Text . AppendLine ( "\t<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>" ) ;
Text . AppendLine ( "\t<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>" ) ;
// Text.AppendLine("\t<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>");
Text . AppendLine ( "\t<uses-permission android:name=\"com.android.vending.CHECK_LICENSE\"/>" ) ;
Text . AppendLine ( "\t<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>" ) ;
if ( bRestoreNotificationsOnReboot )
{
Text . AppendLine ( "\t<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>" ) ;
}
if ( bEnableGooglePlaySupport & & bUseGetAccounts )
{
Text . AppendLine ( "\t<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>" ) ;
}
if ( ! bPackageForOculusMobile )
{
Text . AppendLine ( "\t<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>" ) ;
Text . AppendLine ( "\t<uses-permission android:name=\"android.permission.VIBRATE\"/>" ) ;
}
// Text.AppendLine("\t<uses-permission android:name=\"android.permission.DISABLE_KEYGUARD\"/>");
if ( bEnableIAP )
{
Text . AppendLine ( "\t<uses-permission android:name=\"com.android.vending.BILLING\"/>" ) ;
}
if ( ExtraPermissions ! = null )
{
foreach ( string Permission in ExtraPermissions )
{
string TrimmedPermission = Permission . Trim ( ' ' ) ;
if ( TrimmedPermission ! = "" )
{
string PermissionString = string . Format ( "\t<uses-permission android:name=\"{0}\"/>" , TrimmedPermission ) ;
if ( ! Text . ToString ( ) . Contains ( PermissionString ) )
{
Text . AppendLine ( PermissionString ) ;
}
}
}
}
string RequirementsAdditionsFile = Path . Combine ( GameBuildFilesPath , "ManifestRequirementsAdditions.txt" ) ;
if ( File . Exists ( RequirementsAdditionsFile ) )
{
foreach ( string Line in File . ReadAllLines ( RequirementsAdditionsFile ) )
{
Text . AppendLine ( "\t" + Line ) ;
}
}
if ( AndroidGraphicsDebugger . ToLower ( ) = = "adreno" )
{
string PermissionString = "\t<uses-permission android:name=\"com.qti.permission.PROFILER\"/>" ;
if ( ! Text . ToString ( ) . Contains ( PermissionString ) )
{
Text . AppendLine ( PermissionString ) ;
}
}
if ( ! bSupportingAllTextureFormats )
{
Text . AppendLine ( "\t<!-- Supported texture compression formats (cooked) -->" ) ;
if ( bETC2Enabled & & ! bOnlyETC2Enabled )
{
Text . AppendLine ( "\t<supports-gl-texture android:name=\"GL_COMPRESSED_RGB8_ETC2\" />" ) ;
Text . AppendLine ( "\t<supports-gl-texture android:name=\"GL_COMPRESSED_RGBA8_ETC2_EAC\" />" ) ;
}
if ( bDXTEnabled )
{
Text . AppendLine ( "\t<supports-gl-texture android:name=\"GL_EXT_texture_compression_dxt1\" />" ) ;
Text . AppendLine ( "\t<supports-gl-texture android:name=\"GL_EXT_texture_compression_s3tc\" />" ) ;
Text . AppendLine ( "\t<supports-gl-texture android:name=\"GL_NV_texture_compression_s3tc\" />" ) ;
}
if ( bASTCEnabled )
{
Text . AppendLine ( "\t<supports-gl-texture android:name=\"GL_KHR_texture_compression_astc_ldr\" />" ) ;
}
}
}
Text . AppendLine ( "</manifest>" ) ;
// allow plugins to modify final manifest HERE
XDocument XDoc ;
try
{
XDoc = XDocument . Parse ( Text . ToString ( ) ) ;
}
catch ( Exception e )
{
throw new BuildException ( "AndroidManifest.xml is invalid {0}\n{1}" , e , Text . ToString ( ) ) ;
}
UPL ! . ProcessPluginNode ( Arch , "androidManifestUpdates" , "" , ref XDoc ) ;
return XDoc . ToString ( ) ;
}
private string GenerateProguard ( string Arch , string EngineSourcePath , string GameBuildFilesPath )
{
StringBuilder Text = new StringBuilder ( ) ;
string ProguardFile = Path . Combine ( EngineSourcePath , "proguard-project.txt" ) ;
if ( File . Exists ( ProguardFile ) )
{
foreach ( string Line in File . ReadAllLines ( ProguardFile ) )
{
Text . AppendLine ( Line ) ;
}
}
string ProguardAdditionsFile = Path . Combine ( GameBuildFilesPath , "ProguardAdditions.txt" ) ;
if ( File . Exists ( ProguardAdditionsFile ) )
{
foreach ( string Line in File . ReadAllLines ( ProguardAdditionsFile ) )
{
Text . AppendLine ( Line ) ;
}
}
// add plugin additions
return UPL ! . ProcessPluginNode ( Arch , "proguardAdditions" , Text . ToString ( ) ) ;
}
private void ValidateGooglePlay ( string UnrealBuildPath )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
bool bEnableGooglePlaySupport ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bEnableGooglePlaySupport" , out bEnableGooglePlaySupport ) ;
if ( ! bEnableGooglePlaySupport )
{
// do not need to do anything; it is fine
return ;
}
string IniAppId ;
bool bInvalidIniAppId = false ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "GamesAppID" , out IniAppId ) ;
//validate the value found in the AndroidRuntimeSettings
Int64 Value ;
if ( IniAppId . Length = = 0 | | ! Int64 . TryParse ( IniAppId , out Value ) )
{
bInvalidIniAppId = true ;
}
bool bInvalid = false ;
string ReplacementId = "" ;
String Filename = Path . Combine ( UnrealBuildPath , "res" , "values" , "GooglePlayAppID.xml" ) ;
if ( File . Exists ( Filename ) )
{
string [ ] FileContent = File . ReadAllLines ( Filename ) ;
int LineIndex = - 1 ;
foreach ( string Line in FileContent )
{
+ + LineIndex ;
int StartIndex = Line . IndexOf ( "\"app_id\">" ) ;
if ( StartIndex < 0 )
continue ;
StartIndex + = 9 ;
int EndIndex = Line . IndexOf ( "</string>" ) ;
if ( EndIndex < 0 )
continue ;
string XmlAppId = Line . Substring ( StartIndex , EndIndex - StartIndex ) ;
//validate that the AppId matches the .ini value for the GooglePlay AppId, assuming it's valid
if ( ! bInvalidIniAppId & & IniAppId . CompareTo ( XmlAppId ) ! = 0 )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Replacing Google Play AppID in GooglePlayAppID.xml with AndroidRuntimeSettings .ini value" ) ;
2022-05-24 19:41:41 -04:00
bInvalid = true ;
ReplacementId = IniAppId ;
}
else if ( XmlAppId . Length = = 0 | | ! Int64 . TryParse ( XmlAppId , out Value ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "\nWARNING: GooglePlay Games App ID is invalid! Replacing it with \"1\"" ) ;
2022-05-24 19:41:41 -04:00
//write file with something which will fail but not cause an exception if executed
bInvalid = true ;
ReplacementId = "1" ;
}
if ( bInvalid )
{
// remove any read only flags if invalid so it can be replaced
FileInfo DestFileInfo = new FileInfo ( Filename ) ;
DestFileInfo . Attributes = DestFileInfo . Attributes & ~ FileAttributes . ReadOnly ;
//preserve the rest of the file, just fix up this line
string NewLine = Line . Replace ( "\"app_id\">" + XmlAppId + "</string>" , "\"app_id\">" + ReplacementId + "</string>" ) ;
FileContent [ LineIndex ] = NewLine ;
File . WriteAllLines ( Filename , FileContent ) ;
}
break ;
}
}
else
{
string NewAppId ;
// if we don't have an appID to use from the config, write file with something which will fail but not cause an exception if executed
if ( bInvalidIniAppId )
{
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "\nWARNING: Creating GooglePlayAppID.xml using a Google Play AppID of \"1\" because there was no valid AppID in AndroidRuntimeSettings!" ) ;
2022-05-24 19:41:41 -04:00
NewAppId = "1" ;
}
else
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Creating GooglePlayAppID.xml with AndroidRuntimeSettings .ini value" ) ;
2022-05-24 19:41:41 -04:00
NewAppId = IniAppId ;
}
File . WriteAllText ( Filename , XML_HEADER + "\n<resources>\n\t<string name=\"app_id\">" + NewAppId + "</string>\n</resources>\n" ) ;
}
}
private bool FilesAreDifferent ( string SourceFilename , string DestFilename )
{
// source must exist
FileInfo SourceInfo = new FileInfo ( SourceFilename ) ;
if ( ! SourceInfo . Exists )
{
throw new BuildException ( "Can't make an APK without file [{0}]" , SourceFilename ) ;
}
// different if destination doesn't exist
FileInfo DestInfo = new FileInfo ( DestFilename ) ;
if ( ! DestInfo . Exists )
{
return true ;
}
// file lengths differ?
if ( SourceInfo . Length ! = DestInfo . Length )
{
return true ;
}
// validate timestamps
TimeSpan Diff = DestInfo . LastWriteTimeUtc - SourceInfo . LastWriteTimeUtc ;
if ( Diff . TotalSeconds < - 1 | | Diff . TotalSeconds > 1 )
{
return true ;
}
// could check actual bytes just to be sure, but good enough
return false ;
}
private bool FilesAreIdentical ( string SourceFilename , string DestFilename )
{
// source must exist
FileInfo SourceInfo = new FileInfo ( SourceFilename ) ;
if ( ! SourceInfo . Exists )
{
throw new BuildException ( "Can't make an APK without file [{0}]" , SourceFilename ) ;
}
// different if destination doesn't exist
FileInfo DestInfo = new FileInfo ( DestFilename ) ;
if ( ! DestInfo . Exists )
{
return false ;
}
// file lengths differ?
if ( SourceInfo . Length ! = DestInfo . Length )
{
return false ;
}
using ( FileStream SourceStream = new FileStream ( SourceFilename , FileMode . Open , FileAccess . Read , FileShare . Read ) )
using ( FileStream DestStream = new FileStream ( DestFilename , FileMode . Open , FileAccess . Read , FileShare . Read ) )
{
using ( BinaryReader SourceReader = new BinaryReader ( SourceStream ) )
using ( BinaryReader DestReader = new BinaryReader ( DestStream ) )
{
bool bEOF = false ;
while ( ! bEOF )
{
byte [ ] SourceData = SourceReader . ReadBytes ( 32768 ) ;
if ( SourceData . Length = = 0 )
{
bEOF = true ;
break ;
}
byte [ ] DestData = DestReader . ReadBytes ( 32768 ) ;
if ( ! SourceData . SequenceEqual ( DestData ) )
{
return false ;
}
}
return true ;
}
}
}
private bool RequiresOBB ( bool bDisallowPackageInAPK , string OBBLocation )
{
if ( bDisallowPackageInAPK )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "APK contains data." ) ;
2022-05-24 19:41:41 -04:00
return false ;
}
else if ( ! String . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "uebp_LOCAL_ROOT" ) ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "On build machine." ) ;
2022-05-24 19:41:41 -04:00
return true ;
}
else
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Looking for OBB." ) ;
2022-05-24 19:41:41 -04:00
return File . Exists ( OBBLocation ) ;
}
}
private bool CreateRunGradle ( string GradlePath )
{
string RunGradleBatFilename = Path . Combine ( GradlePath , "rungradle.bat" ) ;
// check for an unused drive letter
string UnusedDriveLetter = "" ;
bool bFound = true ;
DriveInfo [ ] AllDrives = DriveInfo . GetDrives ( ) ;
for ( char DriveLetter = 'Z' ; DriveLetter > = 'A' ; DriveLetter - - )
{
UnusedDriveLetter = Char . ToString ( DriveLetter ) + ":" ;
bFound = false ;
for ( int DriveIndex = AllDrives . Length - 1 ; DriveIndex > = 0 ; DriveIndex - - )
{
if ( AllDrives [ DriveIndex ] . Name . ToUpper ( ) . StartsWith ( UnusedDriveLetter ) )
{
bFound = true ;
break ;
}
}
if ( ! bFound )
{
break ;
}
}
if ( bFound )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\nUnable to apply subst, using gradlew.bat directly (all drive letters in use!)" ) ;
2022-05-24 19:41:41 -04:00
return false ;
}
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\nCreating rungradle.bat to work around commandline length limit (using unused drive letter {UnusedDriveLetter})" , UnusedDriveLetter ) ;
2022-05-24 19:41:41 -04:00
// make sure rungradle.bat isn't read-only
if ( File . Exists ( RunGradleBatFilename ) )
{
FileAttributes Attribs = File . GetAttributes ( RunGradleBatFilename ) ;
if ( Attribs . HasFlag ( FileAttributes . ReadOnly ) )
{
File . SetAttributes ( RunGradleBatFilename , Attribs & ~ FileAttributes . ReadOnly ) ;
}
}
// generate new rungradle.bat with an unused drive letter for subst
string RunGradleBatText =
"@echo off\n" +
"setlocal\n" +
"set GRADLEPATH=%~dp0\n" +
"set GRADLE_CMD_LINE_ARGS=\n" +
":setupArgs\n" +
"if \"\"%1\"\"==\"\"\"\" goto doneStart\n" +
"set GRADLE_CMD_LINE_ARGS=%GRADLE_CMD_LINE_ARGS% %1\n" +
"shift\n" +
"goto setupArgs\n\n" +
":doneStart\n" +
"subst " + UnusedDriveLetter + " \"%CD%\"\n" +
"pushd " + UnusedDriveLetter + "\n" +
"call \"%GRADLEPATH%\\gradlew.bat\" %GRADLE_CMD_LINE_ARGS%\n" +
"set GRADLEERROR=%ERRORLEVEL%\n" +
"popd\n" +
"subst " + UnusedDriveLetter + " /d\n" +
"exit /b %GRADLEERROR%\n" ;
File . WriteAllText ( RunGradleBatFilename , RunGradleBatText ) ;
return true ;
}
private bool GradleEnabled ( )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
bool bEnableGradle = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bEnableGradle" , out bEnableGradle ) ;
return bEnableGradle ;
}
private bool BundleEnabled ( )
{
if ( ForceAPKGeneration )
{
return false ;
}
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
bool bEnableBundle = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bEnableBundle" , out bEnableBundle ) ;
return bEnableBundle ;
}
private bool IsLicenseAgreementValid ( )
{
string LicensePath = Environment . ExpandEnvironmentVariables ( "%ANDROID_HOME%/licenses" ) ;
// directory must exist
if ( ! Directory . Exists ( LicensePath ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Directory doesn't exist {LicensePath}" , LicensePath ) ;
2022-05-24 19:41:41 -04:00
return false ;
}
// license file must exist
string LicenseFilename = Path . Combine ( LicensePath , "android-sdk-license" ) ;
if ( ! File . Exists ( LicenseFilename ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "File doesn't exist {LicenseFilename}" , LicenseFilename ) ;
2022-05-24 19:41:41 -04:00
return false ;
}
// ignore contents of hash for now (Gradle will report if it isn't valid)
return true ;
}
private void GetMinTargetSDKVersions ( AndroidToolChain ToolChain , string Arch , UnrealPluginLanguage UPL , string NDKArch , bool bEnableBundle , out int MinSDKVersion , out int TargetSDKVersion , out int NDKLevelInt )
{
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
Ini . GetInt32 ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "MinSDKVersion" , out MinSDKVersion ) ;
TargetSDKVersion = MinSDKVersion ;
Ini . GetInt32 ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "TargetSDKVersion" , out TargetSDKVersion ) ;
// Check for targetSDKOverride from UPL
string TargetOverride = UPL . ProcessPluginNode ( NDKArch , "targetSDKOverride" , "" ) ;
if ( ! String . IsNullOrEmpty ( TargetOverride ) )
{
int OverrideInt = 0 ;
if ( int . TryParse ( TargetOverride , out OverrideInt ) )
{
TargetSDKVersion = OverrideInt ;
}
}
if ( ( ToolChain . BuildWithSanitizer ( ) ! = AndroidToolChain . ClangSanitizer . None ) & & ( MinSDKVersion < 27 ) )
{
MinSDKVersion = 27 ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Fixing minSdkVersion; requires minSdkVersion of {MinVer} for Clang's Sanitizers" , MinSDKVersion ) ;
2022-05-24 19:41:41 -04:00
}
if ( bEnableBundle & & MinSDKVersion < MinimumSDKLevelForBundle )
{
MinSDKVersion = MinimumSDKLevelForBundle ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Fixing minSdkVersion; requires minSdkVersion of {MinVer} for App Bundle support" , MinimumSDKLevelForBundle ) ;
2022-05-24 19:41:41 -04:00
}
// Make sure minSdkVersion is at least 13 (need this for appcompat-v13 used by AndroidPermissions)
// this may be changed by active plugins (Google Play Services 11.0.4 needs 14 for example)
if ( MinSDKVersion < MinimumSDKLevelForGradle )
{
MinSDKVersion = MinimumSDKLevelForGradle ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Fixing minSdkVersion; requires minSdkVersion of {MinVer} with Gradle based on active plugins" , MinimumSDKLevelForGradle ) ;
2022-05-24 19:41:41 -04:00
}
// 64-bit targets must be android-21 or higher
NDKLevelInt = ToolChain . GetNdkApiLevelInt ( ) ;
if ( NDKLevelInt < 21 )
{
// 21 is requred for GL ES3.1
NDKLevelInt = 21 ;
}
// fix up the MinSdkVersion
if ( NDKLevelInt > 19 )
{
if ( MinSDKVersion < 21 )
{
MinSDKVersion = 21 ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Fixing minSdkVersion; NDK level above 19 requires minSdkVersion of 21 (arch={Arch})" , Arch . Substring ( 1 ) ) ;
2022-05-24 19:41:41 -04:00
}
if ( MinSDKVersion < NDKLevelInt )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Fixing minSdkVersion; NDK level is {NdkLevel} which is above minSdkVersion {MinSdkVersion}." , NDKLevelInt , MinSDKVersion ) ;
2022-05-24 19:41:41 -04:00
MinSDKVersion = NDKLevelInt ;
}
}
if ( TargetSDKVersion < MinSDKVersion )
{
TargetSDKVersion = MinSDKVersion ;
}
}
private void CreateGradlePropertiesFiles ( string Arch , int MinSDKVersion , int TargetSDKVersion , string CompileSDKVersion , string BuildToolsVersion , string PackageName ,
string? DestApkName , string NDKArch , string UnrealBuildFilesPath , string GameBuildFilesPath , string UnrealBuildGradleAppPath , string UnrealBuildPath , string UnrealBuildGradlePath ,
bool bForDistribution , bool bIsEmbedded , List < string > OBBFiles )
{
// Create gradle.properties
StringBuilder GradleProperties = new StringBuilder ( ) ;
int StoreVersion = GetStoreVersion ( GetUnrealArch ( NDKArch ) ) ;
string VersionDisplayName = GetVersionDisplayName ( bIsEmbedded ) ;
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
bool bEnableUniversalAPK = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bEnableUniversalAPK" , out bEnableUniversalAPK ) ;
GradleProperties . AppendLine ( "org.gradle.daemon=false" ) ;
GradleProperties . AppendLine ( "org.gradle.jvmargs=-XX:MaxHeapSize=4096m -Xmx9216m" ) ;
GradleProperties . AppendLine ( "android.injected.testOnly=false" ) ;
GradleProperties . AppendLine ( "android.useAndroidX=true" ) ;
GradleProperties . AppendLine ( "android.enableJetifier=true" ) ;
GradleProperties . AppendLine ( string . Format ( "COMPILE_SDK_VERSION={0}" , CompileSDKVersion ) ) ;
GradleProperties . AppendLine ( string . Format ( "BUILD_TOOLS_VERSION={0}" , BuildToolsVersion ) ) ;
GradleProperties . AppendLine ( string . Format ( "PACKAGE_NAME={0}" , PackageName ) ) ;
GradleProperties . AppendLine ( string . Format ( "MIN_SDK_VERSION={0}" , MinSDKVersion . ToString ( ) ) ) ;
GradleProperties . AppendLine ( string . Format ( "TARGET_SDK_VERSION={0}" , TargetSDKVersion . ToString ( ) ) ) ;
GradleProperties . AppendLine ( string . Format ( "STORE_VERSION={0}" , StoreVersion . ToString ( ) ) ) ;
GradleProperties . AppendLine ( string . Format ( "VERSION_DISPLAY_NAME={0}" , VersionDisplayName ) ) ;
if ( DestApkName ! = null )
{
GradleProperties . AppendLine ( string . Format ( "OUTPUT_PATH={0}" , Path . GetDirectoryName ( DestApkName ) ! . Replace ( "\\" , "/" ) ) ) ;
GradleProperties . AppendLine ( string . Format ( "OUTPUT_FILENAME={0}" , Path . GetFileName ( DestApkName ) ) ) ;
string BundleFilename = Path . GetFileName ( DestApkName ) . Replace ( ".apk" , ".aab" ) ;
GradleProperties . AppendLine ( string . Format ( "OUTPUT_BUNDLEFILENAME={0}" , BundleFilename ) ) ;
if ( bEnableUniversalAPK )
{
string UniversalAPKFilename = Path . GetFileName ( DestApkName ) . Replace ( ".apk" , "_universal.apk" ) ;
GradleProperties . AppendLine ( "OUTPUT_UNIVERSALFILENAME=" + UniversalAPKFilename ) ;
}
}
int OBBFileIndex = 0 ;
GradleProperties . AppendLine ( string . Format ( "OBB_FILECOUNT={0}" , OBBFiles . Count ) ) ;
foreach ( string OBBFile in OBBFiles )
{
GradleProperties . AppendLine ( string . Format ( "OBB_FILE{0}={1}" , OBBFileIndex + + , OBBFile . Replace ( "\\" , "/" ) ) ) ;
}
GradleProperties . AppendLine ( "ANDROID_TOOLS_BUILD_GRADLE_VERSION={0}" , ANDROID_TOOLS_BUILD_GRADLE_VERSION ) ;
GradleProperties . AppendLine ( "BUNDLETOOL_JAR=" + Path . GetFullPath ( Path . Combine ( UnrealBuildFilesPath , ".." , "Prebuilt" , "bundletool" , BUNDLETOOL_JAR ) ) . Replace ( "\\" , "/" ) ) ;
GradleProperties . AppendLine ( "GENUNIVERSALAPK_JAR=" + Path . GetFullPath ( Path . Combine ( UnrealBuildFilesPath , ".." , "Prebuilt" , "GenUniversalAPK" , "bin" , "GenUniversalAPK.jar" ) ) . Replace ( "\\" , "/" ) ) ;
// add any Gradle properties from UPL
string GradlePropertiesUPL = UPL ! . ProcessPluginNode ( NDKArch , "gradleProperties" , "" ) ;
GradleProperties . AppendLine ( GradlePropertiesUPL ) ;
// Create abi.gradle
StringBuilder ABIGradle = new StringBuilder ( ) ;
ABIGradle . AppendLine ( "android {" ) ;
ABIGradle . AppendLine ( "\tdefaultConfig {" ) ;
ABIGradle . AppendLine ( "\t\tndk {" ) ;
ABIGradle . AppendLine ( string . Format ( "\t\t\tabiFilter \"{0}\"" , NDKArch ) ) ;
ABIGradle . AppendLine ( "\t\t}" ) ;
ABIGradle . AppendLine ( "\t}" ) ;
ABIGradle . AppendLine ( "}" ) ;
string ABIGradleFilename = Path . Combine ( UnrealBuildGradleAppPath , "abi.gradle" ) ;
File . WriteAllText ( ABIGradleFilename , ABIGradle . ToString ( ) ) ;
StringBuilder GradleBuildAdditionsContent = new StringBuilder ( ) ;
GradleBuildAdditionsContent . AppendLine ( "apply from: 'aar-imports.gradle'" ) ;
GradleBuildAdditionsContent . AppendLine ( "apply from: 'projects.gradle'" ) ;
GradleBuildAdditionsContent . AppendLine ( "apply from: 'abi.gradle'" ) ;
bool bEnableBundle , bBundleABISplit , bBundleLanguageSplit , bBundleDensitySplit ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bEnableBundle" , out bEnableBundle ) ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bBundleABISplit" , out bBundleABISplit ) ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bBundleLanguageSplit" , out bBundleLanguageSplit ) ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bBundleDensitySplit" , out bBundleDensitySplit ) ;
GradleBuildAdditionsContent . AppendLine ( "android {" ) ;
if ( ! ForceAPKGeneration & & bEnableBundle )
{
GradleBuildAdditionsContent . AppendLine ( "\tbundle {" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\tabi { enableSplit = " + ( bBundleABISplit ? "true" : "false" ) + " }" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\tlanguage { enableSplit = " + ( bBundleLanguageSplit ? "true" : "false" ) + " }" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\tdensity { enableSplit = " + ( bBundleDensitySplit ? "true" : "false" ) + " }" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t}" ) ;
}
if ( bForDistribution )
{
bool bDisableV2Signing = false ;
if ( GetTargetOculusMobileDevices ( ) . Contains ( "Go" ) )
{
bDisableV2Signing = true ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Disabling v2Signing for Oculus Go" ) ;
2022-05-24 19:41:41 -04:00
}
string KeyAlias , KeyStore , KeyStorePassword , KeyPassword ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "KeyStore" , out KeyStore ) ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "KeyAlias" , out KeyAlias ) ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "KeyStorePassword" , out KeyStorePassword ) ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "KeyPassword" , out KeyPassword ) ;
if ( string . IsNullOrEmpty ( KeyStore ) | | string . IsNullOrEmpty ( KeyAlias ) | | string . IsNullOrEmpty ( KeyStorePassword ) )
{
throw new BuildException ( "DistributionSigning settings are not all set. Check the DistributionSettings section in the Android tab of Project Settings" ) ;
}
if ( string . IsNullOrEmpty ( KeyPassword ) | | KeyPassword = = "_sameaskeystore_" )
{
KeyPassword = KeyStorePassword ;
}
// Make sure the keystore file exists
string KeyStoreFilename = Path . Combine ( UnrealBuildPath , KeyStore ) ;
if ( ! File . Exists ( KeyStoreFilename ) )
{
throw new BuildException ( "Keystore file is missing. Check the DistributionSettings section in the Android tab of Project Settings" ) ;
}
GradleProperties . AppendLine ( string . Format ( "STORE_FILE={0}" , KeyStoreFilename . Replace ( "\\" , "/" ) ) ) ;
GradleProperties . AppendLine ( string . Format ( "STORE_PASSWORD={0}" , KeyStorePassword ) ) ;
GradleProperties . AppendLine ( string . Format ( "KEY_ALIAS={0}" , KeyAlias ) ) ;
GradleProperties . AppendLine ( string . Format ( "KEY_PASSWORD={0}" , KeyPassword ) ) ;
GradleBuildAdditionsContent . AppendLine ( "\tsigningConfigs {" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\trelease {" ) ;
GradleBuildAdditionsContent . AppendLine ( string . Format ( "\t\t\tstoreFile file('{0}')" , KeyStoreFilename . Replace ( "\\" , "/" ) ) ) ;
GradleBuildAdditionsContent . AppendLine ( string . Format ( "\t\t\tstorePassword '{0}'" , KeyStorePassword ) ) ;
GradleBuildAdditionsContent . AppendLine ( string . Format ( "\t\t\tkeyAlias '{0}'" , KeyAlias ) ) ;
GradleBuildAdditionsContent . AppendLine ( string . Format ( "\t\t\tkeyPassword '{0}'" , KeyPassword ) ) ;
if ( bDisableV2Signing )
{
GradleBuildAdditionsContent . AppendLine ( "\t\t\tv2SigningEnabled false" ) ;
}
GradleBuildAdditionsContent . AppendLine ( "\t\t}" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t}" ) ;
// Generate the Proguard file contents and write it
string ProguardContents = GenerateProguard ( NDKArch , UnrealBuildFilesPath , GameBuildFilesPath ) ;
string ProguardFilename = Path . Combine ( UnrealBuildGradleAppPath , "proguard-rules.pro" ) ;
SafeDeleteFile ( ProguardFilename ) ;
File . WriteAllText ( ProguardFilename , ProguardContents ) ;
}
else
{
// empty just for Gradle not to complain
GradleProperties . AppendLine ( "STORE_FILE=" ) ;
GradleProperties . AppendLine ( "STORE_PASSWORD=" ) ;
GradleProperties . AppendLine ( "KEY_ALIAS=" ) ;
GradleProperties . AppendLine ( "KEY_PASSWORD=" ) ;
// empty just for Gradle not to complain
GradleBuildAdditionsContent . AppendLine ( "\tsigningConfigs {" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\trelease {" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\t}" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t}" ) ;
}
GradleBuildAdditionsContent . AppendLine ( "\tbuildTypes {" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\trelease {" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\t\tsigningConfig signingConfigs.release" ) ;
if ( GradlePropertiesUPL . Contains ( "DISABLE_MINIFY=1" ) )
{
GradleBuildAdditionsContent . AppendLine ( "\t\t\tminifyEnabled false" ) ;
}
else
{
GradleBuildAdditionsContent . AppendLine ( "\t\t\tminifyEnabled true" ) ;
}
if ( GradlePropertiesUPL . Contains ( "DISABLE_PROGUARD=1" ) )
{
GradleBuildAdditionsContent . AppendLine ( "\t\t\tuseProguard false" ) ;
}
else
{
GradleBuildAdditionsContent . AppendLine ( "\t\t\tproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'" ) ;
}
GradleBuildAdditionsContent . AppendLine ( "\t\t}" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\tdebug {" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\t\tdebuggable true" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t\t}" ) ;
GradleBuildAdditionsContent . AppendLine ( "\t}" ) ;
GradleBuildAdditionsContent . AppendLine ( "}" ) ;
// Add any UPL app buildGradleAdditions
GradleBuildAdditionsContent . Append ( UPL . ProcessPluginNode ( NDKArch , "buildGradleAdditions" , "" ) ) ;
string GradleBuildAdditionsFilename = Path . Combine ( UnrealBuildGradleAppPath , "buildAdditions.gradle" ) ;
File . WriteAllText ( GradleBuildAdditionsFilename , GradleBuildAdditionsContent . ToString ( ) ) ;
string GradlePropertiesFilename = Path . Combine ( UnrealBuildGradlePath , "gradle.properties" ) ;
File . WriteAllText ( GradlePropertiesFilename , GradleProperties . ToString ( ) ) ;
// Add lint if requested (note depreciation warnings can be suppressed with @SuppressWarnings("deprecation")
string GradleBaseBuildAdditionsContents = "" ;
bool bEnableLint = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bEnableLint" , out bEnableLint ) ;
if ( bEnableLint )
{
GradleBaseBuildAdditionsContents =
"allprojects {\n" +
"\ttasks.withType(JavaCompile) {\n" +
"\t\toptions.compilerArgs << \"-Xlint:unchecked\" << \"-Xlint:deprecation\"\n" +
"\t}\n" +
"}\n\n" ;
}
// Create baseBuildAdditions.gradle from plugins baseBuildGradleAdditions
string GradleBaseBuildAdditionsFilename = Path . Combine ( UnrealBuildGradlePath , "baseBuildAdditions.gradle" ) ;
File . WriteAllText ( GradleBaseBuildAdditionsFilename , UPL . ProcessPluginNode ( NDKArch , "baseBuildGradleAdditions" , GradleBaseBuildAdditionsContents ) ) ;
// Create buildscriptAdditions.gradle from plugins buildscriptGradleAdditions
string GradleBuildScriptAdditionsFilename = Path . Combine ( UnrealBuildGradlePath , "buildscriptAdditions.gradle" ) ;
File . WriteAllText ( GradleBuildScriptAdditionsFilename , UPL . ProcessPluginNode ( NDKArch , "buildscriptGradleAdditions" , "" ) ) ;
}
private void MakeApk ( AndroidToolChain ToolChain , string ProjectName , TargetType InTargetType , string ProjectDirectory , string OutputPath , string EngineDirectory , bool bForDistribution , string CookFlavor ,
UnrealTargetConfiguration Configuration , bool bMakeSeparateApks , bool bIncrementalPackage , bool bDisallowPackagingDataInApk , bool bDisallowExternalFilesDir , bool bSkipGradleBuild )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "" ) ;
Logger . LogInformation ( "===={Time}====PREPARING TO MAKE APK=================================================================" , DateTime . Now . ToString ( ) ) ;
2022-05-24 19:41:41 -04:00
// Get list of all architecture and GPU targets for build
List < string > Arches = ToolChain . GetAllArchitectures ( ) ;
// we do not need to really build an engine UnrealGame.apk so short-circuit it
if ( ! ForceAPKGeneration & & ProjectName = = "UnrealGame" & & OutputPath . Replace ( "\\" , "/" ) . Contains ( "/Engine/Binaries/Android/" ) & & Path . GetFileNameWithoutExtension ( OutputPath ) . StartsWith ( "UnrealGame" ) )
{
if ( ! bSkipGradleBuild )
{
/ *
IEnumerable < Tuple < string , string > > TargetList = null ;
TargetList = from Arch in Arches
from GPUArch in GPUArchitectures
select Tuple . Create ( Arch , GPUArch ) ;
string DestApkDirectory = Path . Combine ( ProjectDirectory , "Binaries/Android" ) ;
string ApkFilename = Path . GetFileNameWithoutExtension ( OutputPath ) . Replace ( "UnrealGame" , ProjectName ) ;
foreach ( Tuple < string , string > target in TargetList )
{
string Arch = target . Item1 ;
string GPUArchitecture = target . Item2 ;
string DestApkName = Path . Combine ( DestApkDirectory , ApkFilename + ".apk" ) ;
DestApkName = AndroidToolChain . InlineArchName ( DestApkName , Arch , GPUArchitecture ) ;
// create a dummy APK if doesn't exist
if ( ! File . Exists ( DestApkName ) )
{
File . WriteAllText ( DestApkName , "dummyfile" ) ;
}
}
* /
}
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "APK generation not needed for project {ProjectName} with {OutputPath}" , ProjectName , OutputPath ) ;
Logger . LogInformation ( "" ) ;
Logger . LogInformation ( "===={Time}====COMPLETED MAKE APK=======================================================================" , DateTime . Now . ToString ( ) ) ;
2022-05-24 19:41:41 -04:00
return ;
}
if ( ! GradleEnabled ( ) )
{
throw new BuildException ( "Support for building APK without Gradle is depreciated; please update your project Engine.ini." ) ;
}
if ( UPL ! . GetLastError ( ) ! = null )
{
throw new BuildException ( "Cannot make APK with UPL errors" ) ;
}
// make sure it is cached (clear unused warning)
string EngineVersion = ReadEngineVersion ( ) ;
if ( EngineVersion = = null )
{
throw new BuildException ( "No engine version!" ) ;
}
SetMinimumSDKLevelForGradle ( ) ;
// Verify license agreement since we require Gradle
if ( ! IsLicenseAgreementValid ( ) )
{
throw new BuildException ( "Android SDK license file not found. Please agree to license in Android project settings in the editor." ) ;
}
LogBuildSetup ( ) ;
// bundles disabled for launch-on
bool bEnableBundle = BundleEnabled ( ) & & ! bDisallowPackagingDataInApk ;
bool bIsBuildMachine = Environment . GetEnvironmentVariable ( "IsBuildMachine" ) = = "1" ;
// do this here so we'll stop early if there is a problem with the SDK API level (cached so later calls will return the same)
string SDKAPILevel = GetSdkApiLevel ( ToolChain ) ;
int SDKLevelInt = GetApiLevelInt ( SDKAPILevel ) ;
string BuildToolsVersion = GetBuildToolsVersion ( ) ;
// cache some tools paths
//string NDKBuildPath = Environment.ExpandEnvironmentVariables("%NDKROOT%/ndk-build" + (RuntimePlatform.IsWindows ? ".cmd" : ""));
//bool HasNDKPath = File.Exists(NDKBuildPath);
// set up some directory info
string IntermediateAndroidPath = Path . Combine ( ProjectDirectory , "Intermediate" , "Android" ) ;
string UnrealJavaFilePath = Path . Combine ( ProjectDirectory , "Build" , "Android" , GetUnrealJavaSrcPath ( ) ) ;
string UnrealBuildFilesPath = GetUnrealBuildFilePath ( EngineDirectory ) ;
string UnrealBuildFilesPath_NFL = GetUnrealBuildFilePath ( Path . Combine ( EngineDirectory , "Restricted/NotForLicensees" ) ) ;
string UnrealBuildFilesPath_NR = GetUnrealBuildFilePath ( Path . Combine ( EngineDirectory , "Restricted/NoRedist" ) ) ;
string GameBuildFilesPath = Path . Combine ( ProjectDirectory , "Build" , "Android" ) ;
string GameBuildFilesPath_NFL = Path . Combine ( Path . Combine ( ProjectDirectory , "Restricted/NotForLicensees" ) , "Build" , "Android" ) ;
string GameBuildFilesPath_NR = Path . Combine ( Path . Combine ( ProjectDirectory , "Restricted/NoRedist" ) , "Build" , "Android" ) ;
// get a list of unique NDK architectures enabled for build
List < string > NDKArches = new List < string > ( ) ;
foreach ( string Arch in Arches )
{
string NDKArch = GetNDKArch ( Arch ) ;
if ( ! NDKArches . Contains ( NDKArch ) )
{
NDKArches . Add ( NDKArch ) ;
}
}
// force create from scratch if on build machine
bool bCreateFromScratch = bIsBuildMachine ;
AndroidToolChain . ClangSanitizer Sanitizer = ToolChain . BuildWithSanitizer ( ) ;
// see if last time matches the skipGradle setting
string BuildTypeFilename = Path . Combine ( IntermediateAndroidPath , "BuildType.txt" ) ;
string BuildTypeID = bSkipGradleBuild ? "Embedded" : "Standalone" ;
if ( Sanitizer ! = AndroidToolChain . ClangSanitizer . None & & Sanitizer ! = AndroidToolChain . ClangSanitizer . HwAddress )
{
BuildTypeID + = Sanitizer . ToString ( ) + "Sanitizer" ;
}
if ( File . Exists ( BuildTypeFilename ) )
{
string BuildTypeContents = File . ReadAllText ( BuildTypeFilename ) ;
if ( BuildTypeID ! = BuildTypeContents )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Build type changed, forcing clean" ) ;
2022-05-24 19:41:41 -04:00
bCreateFromScratch = true ;
}
}
// force cleanup if older UE4 project
if ( File . Exists ( Path . Combine ( IntermediateAndroidPath , "arm64" , "jni" , "arm64-v8a" , "libUE4.so" ) ) | |
File . Exists ( Path . Combine ( IntermediateAndroidPath , "x64" , "jni" , "x86_64" , "libUE4.so" ) ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Old version of library .so found, forcing clean" ) ;
2022-05-24 19:41:41 -04:00
bCreateFromScratch = true ;
}
// check if the enabled plugins has changed
string PluginListFilename = Path . Combine ( IntermediateAndroidPath , "ActiveUPL.txt" ) ;
string PluginListContents = ActiveUPLFiles . ToString ( ) ;
if ( File . Exists ( PluginListFilename ) )
{
string PreviousPluginListContents = File . ReadAllText ( PluginListFilename ) ;
if ( PluginListContents ! = PreviousPluginListContents )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Active UPL files changed, forcing clean" ) ;
2022-05-24 19:41:41 -04:00
bCreateFromScratch = true ;
}
}
if ( bCreateFromScratch )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Cleaning {IntermediateAndroidPath}" , IntermediateAndroidPath ) ;
DeleteDirectory ( IntermediateAndroidPath , Logger ) ;
2022-05-24 19:41:41 -04:00
Directory . CreateDirectory ( IntermediateAndroidPath ) ;
}
if ( ! System . IO . Directory . Exists ( IntermediateAndroidPath ) )
{
System . IO . Directory . CreateDirectory ( IntermediateAndroidPath ) ;
}
// write enabled plugins list
File . WriteAllText ( PluginListFilename , PluginListContents ) ;
// write build type
File . WriteAllText ( BuildTypeFilename , BuildTypeID ) ;
// cache if we want data in the Apk
bool bPackageDataInsideApk = bDisallowPackagingDataInApk ? false : GetPackageDataInsideApk ( ) ;
bool bDisableVerifyOBBOnStartUp = DisableVerifyOBBOnStartUp ( ) ;
bool bUseExternalFilesDir = UseExternalFilesDir ( bDisallowExternalFilesDir ) ;
// Generate Java files
string PackageName = GetPackageName ( ProjectName ) ;
string TemplateDestinationBase = Path . Combine ( ProjectDirectory , "Build" , "Android" , "src" , PackageName . Replace ( '.' , Path . DirectorySeparatorChar ) ) ;
MakeDirectoryIfRequired ( TemplateDestinationBase ) ;
// We'll be writing the OBB data into the same location as the download service files
string UnrealOBBDataFileName = GetUnrealJavaOBBDataFileName ( TemplateDestinationBase ) ;
string UnrealDownloadShimFileName = GetUnrealJavaDownloadShimFileName ( UnrealJavaFilePath ) ;
// Template generated files
string JavaTemplateSourceDir = GetUnrealTemplateJavaSourceDir ( EngineDirectory ) ;
IEnumerable < TemplateFile > templates = from template in Directory . EnumerateFiles ( JavaTemplateSourceDir , "*.template" )
let RealName = Path . GetFileNameWithoutExtension ( template )
select new TemplateFile ( SourceFile : template , DestinationFile : GetUnrealTemplateJavaDestination ( TemplateDestinationBase , RealName ) ) ;
// Generate the OBB and Shim files here
string ObbFileLocation = ProjectDirectory + "/Saved/StagedBuilds/Android" + CookFlavor + ".obb" ;
string PatchFileLocation = ProjectDirectory + "/Saved/StagedBuilds/Android" + CookFlavor + ".patch.obb" ;
List < string > RequiredOBBFiles = new List < String > { ObbFileLocation } ;
if ( File . Exists ( PatchFileLocation ) )
{
RequiredOBBFiles . Add ( PatchFileLocation ) ;
}
if ( Arches . Count > 0 )
{
// Generate the OBBData.java file if out of date (can skip rewriting it if packaging inside Apk in some cases)
// Note: this may be replaced per architecture later if store version is different
WriteJavaOBBDataFile ( UnrealOBBDataFileName , PackageName , RequiredOBBFiles , CookFlavor , bPackageDataInsideApk , Arches [ 0 ] . Substring ( 1 ) ) ;
}
// Make sure any existing proguard file in project is NOT used (back it up)
string ProjectBuildProguardFile = Path . Combine ( GameBuildFilesPath , "proguard-project.txt" ) ;
if ( File . Exists ( ProjectBuildProguardFile ) )
{
string ProjectBackupProguardFile = Path . Combine ( GameBuildFilesPath , "proguard-project.backup" ) ;
File . Move ( ProjectBuildProguardFile , ProjectBackupProguardFile ) ;
}
WriteJavaDownloadSupportFiles ( UnrealDownloadShimFileName , templates , new Dictionary < string , string > {
{ "$$GameName$$" , ProjectName } ,
{ "$$PublicKey$$" , GetPublicKey ( ) } ,
{ "$$PackageName$$" , PackageName }
} ) ;
// Sometimes old files get left behind if things change, so we'll do a clean up pass
foreach ( string NDKArch in NDKArches )
{
string UnrealBuildPath = Path . Combine ( IntermediateAndroidPath , GetUnrealArch ( NDKArch ) . Substring ( 1 ) . Replace ( "-" , "_" ) ) ;
string CleanUpBaseDir = Path . Combine ( ProjectDirectory , "Build" , "Android" , "src" ) ;
string ImmediateBaseDir = Path . Combine ( UnrealBuildPath , "src" ) ;
IEnumerable < string > files = Directory . EnumerateFiles ( CleanUpBaseDir , "*.java" , SearchOption . AllDirectories ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Cleaning up files based on template dir {TemplateDestinationBase}" , TemplateDestinationBase ) ;
2022-05-24 19:41:41 -04:00
// Make a set of files that are okay to clean up
HashSet < string > cleanFiles = new HashSet < string > ( ) ;
cleanFiles . Add ( "DownloadShim.java" ) ;
cleanFiles . Add ( "OBBData.java" ) ;
foreach ( TemplateFile template in templates )
{
cleanFiles . Add ( Path . GetFileName ( template . DestinationFile ) ) ;
}
foreach ( string filename in files )
{
// keep the shim if it is in the right place
if ( filename = = UnrealDownloadShimFileName )
{
continue ;
}
string filePath = Path . GetDirectoryName ( filename ) ! ; // grab the file's path
if ( filePath ! = TemplateDestinationBase ) // and check to make sure it isn't the same as the Template directory we calculated earlier
{
// Only delete the files in the cleanup set
if ( ! cleanFiles . Contains ( Path . GetFileName ( filename ) ) )
continue ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Cleaning up file {File}" , filename ) ;
2022-05-24 19:41:41 -04:00
SafeDeleteFile ( filename , false ) ;
// Check to see if this file also exists in our target destination, and if so delete it too
string DestFilename = Path . Combine ( ImmediateBaseDir , Utils . MakePathRelativeTo ( filename , CleanUpBaseDir ) ) ;
if ( File . Exists ( DestFilename ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Cleaning up file {DestFilename}" , DestFilename ) ;
2022-05-24 19:41:41 -04:00
SafeDeleteFile ( DestFilename , false ) ;
}
}
}
// Directory clean up code (Build/Android/src)
try
{
IEnumerable < string > BaseDirectories = Directory . EnumerateDirectories ( CleanUpBaseDir , "*" , SearchOption . AllDirectories ) . OrderByDescending ( x = > x ) ;
foreach ( string directory in BaseDirectories )
{
if ( Directory . Exists ( directory ) & & Directory . GetFiles ( directory , "*.*" , SearchOption . AllDirectories ) . Count ( ) = = 0 )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Cleaning Directory {Directory} as empty." , directory ) ;
2022-05-24 19:41:41 -04:00
Directory . Delete ( directory , true ) ;
}
}
}
catch ( Exception )
{
// likely System.IO.DirectoryNotFoundException, ignore it
}
// Directory clean up code (Intermediate/APK/src)
try
{
IEnumerable < string > ImmediateDirectories = Directory . EnumerateDirectories ( ImmediateBaseDir , "*" , SearchOption . AllDirectories ) . OrderByDescending ( x = > x ) ;
foreach ( string directory in ImmediateDirectories )
{
if ( Directory . Exists ( directory ) & & Directory . GetFiles ( directory , "*.*" , SearchOption . AllDirectories ) . Count ( ) = = 0 )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Cleaning Directory {Directory} as empty." , directory ) ;
2022-05-24 19:41:41 -04:00
Directory . Delete ( directory , true ) ;
}
}
}
catch ( Exception )
{
// likely System.IO.DirectoryNotFoundException, ignore it
}
}
// check to see if any "meta information" is newer than last time we build
string TemplatesHashCode = GenerateTemplatesHashCode ( EngineDirectory ) ;
string CurrentBuildSettings = GetAllBuildSettings ( ToolChain , UPL ! , bForDistribution , bMakeSeparateApks , bPackageDataInsideApk , bDisableVerifyOBBOnStartUp , bUseExternalFilesDir , TemplatesHashCode ) ;
string BuildSettingsCacheFile = Path . Combine ( IntermediateAndroidPath , "UEBuildSettings.txt" ) ;
// Architecture remapping
Dictionary < string , string > ArchRemapping = new Dictionary < string , string > ( ) ;
ArchRemapping . Add ( "arm64-v8a" , "arm64" ) ;
ArchRemapping . Add ( "x86_64" , "x64" ) ;
// do we match previous build settings?
bool bBuildSettingsMatch = true ;
// get application name and whether it changed, needing to force repackage
string? ApplicationDisplayName ;
if ( CheckApplicationName ( Path . Combine ( IntermediateAndroidPath , ArchRemapping [ NDKArches [ 0 ] ] ) , ProjectName , out ApplicationDisplayName ) )
{
bBuildSettingsMatch = false ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Application display name is different than last build, forcing repackage." ) ;
2022-05-24 19:41:41 -04:00
}
// if the manifest matches, look at other settings stored in a file
if ( bBuildSettingsMatch )
{
if ( File . Exists ( BuildSettingsCacheFile ) )
{
string PreviousBuildSettings = File . ReadAllText ( BuildSettingsCacheFile ) ;
if ( PreviousBuildSettings ! = CurrentBuildSettings )
{
bBuildSettingsMatch = false ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Previous .apk file(s) were made with different build settings, forcing repackage." ) ;
2022-05-24 19:41:41 -04:00
}
}
}
// only check input dependencies if the build settings already match (if we don't run gradle, there is no Apk file to check against)
if ( bBuildSettingsMatch & & ! bSkipGradleBuild )
{
// check if so's are up to date against various inputs
List < string > JavaFiles = new List < string > {
UnrealOBBDataFileName ,
UnrealDownloadShimFileName
} ;
// Add the generated files too
JavaFiles . AddRange ( from t in templates select t . SourceFile ) ;
JavaFiles . AddRange ( from t in templates select t . DestinationFile ) ;
bBuildSettingsMatch = CheckDependencies ( ToolChain , ProjectName , ProjectDirectory , UnrealBuildFilesPath , GameBuildFilesPath ,
EngineDirectory , JavaFiles , CookFlavor , OutputPath , bMakeSeparateApks , bPackageDataInsideApk ) ;
}
string CommandLineSourceFileName = Path . Combine ( Path . GetDirectoryName ( ObbFileLocation ) ! , Path . GetFileNameWithoutExtension ( ObbFileLocation ) , "UECommandLine.txt" ) ;
string CommandLineCacheFileName = Path . Combine ( IntermediateAndroidPath , "UECommandLine.txt" ) ;
if ( bBuildSettingsMatch )
{
bool bCommandLineMatch =
( ! File . Exists ( CommandLineSourceFileName ) & & ! File . Exists ( CommandLineCacheFileName ) ) | |
BinaryFileEquals ( CommandLineSourceFileName , CommandLineCacheFileName ) ;
if ( ! bCommandLineMatch )
{
bBuildSettingsMatch = false ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Previous .apk file(s) were made with different stage/apk command line, forcing repackage." ) ;
2022-05-24 19:41:41 -04:00
}
}
// Initialize UPL contexts for each architecture enabled
UPL . Init ( NDKArches , bForDistribution , EngineDirectory , IntermediateAndroidPath , ProjectDirectory , Configuration . ToString ( ) , bSkipGradleBuild , bPerArchBuildDir : true , ArchRemapping : ArchRemapping ) ;
IEnumerable < Tuple < string , string > > ? BuildList = null ;
bool bRequiresOBB = RequiresOBB ( bDisallowPackagingDataInApk , ObbFileLocation ) ;
if ( ! bBuildSettingsMatch )
{
BuildList = from Arch in Arches
let manifest = GenerateManifest ( ToolChain , ProjectName , InTargetType , EngineDirectory , bForDistribution , bPackageDataInsideApk , GameBuildFilesPath , bRequiresOBB , bDisableVerifyOBBOnStartUp , Arch , CookFlavor , bUseExternalFilesDir , Configuration . ToString ( ) , SDKLevelInt , bSkipGradleBuild , bEnableBundle )
select Tuple . Create ( Arch , manifest ) ;
}
else
{
BuildList = from Arch in Arches
let manifestFile = Path . Combine ( IntermediateAndroidPath , Arch + "_AndroidManifest.xml" )
let manifest = GenerateManifest ( ToolChain , ProjectName , InTargetType , EngineDirectory , bForDistribution , bPackageDataInsideApk , GameBuildFilesPath , bRequiresOBB , bDisableVerifyOBBOnStartUp , Arch , CookFlavor , bUseExternalFilesDir , Configuration . ToString ( ) , SDKLevelInt , bSkipGradleBuild , bEnableBundle )
let OldManifest = File . Exists ( manifestFile ) ? File . ReadAllText ( manifestFile ) : ""
where manifest ! = OldManifest
select Tuple . Create ( Arch , manifest ) ;
}
// Now we have to spin over all the arch/gpu combinations to make sure they all match
int BuildListComboTotal = BuildList . Count ( ) ;
if ( BuildListComboTotal = = 0 )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Output .apk file(s) are up to date (dependencies and build settings are up to date)" ) ;
2022-05-24 19:41:41 -04:00
return ;
}
// at this point, we can write out the cached build settings to compare for a next build
File . WriteAllText ( BuildSettingsCacheFile , CurrentBuildSettings ) ;
if ( File . Exists ( CommandLineSourceFileName ) )
{
CopyIfDifferent ( CommandLineSourceFileName , CommandLineCacheFileName , true , true ) ;
}
else
{
SafeDeleteFile ( CommandLineCacheFileName ) ;
}
// make up a dictionary of strings to replace in xml files (strings.xml)
Dictionary < string , string > Replacements = new Dictionary < string , string > ( ) ;
Replacements . Add ( "${EXECUTABLE_NAME}" , ApplicationDisplayName ! ) ;
Replacements . Add ( "${PY_VISUALIZER_PATH}" , Path . GetFullPath ( Path . Combine ( EngineDirectory , "Extras" , "LLDBDataFormatters" , "UEDataFormatters_2ByteChars.py" ) ) ) ;
// steps run for each build combination (note: there should only be one GPU in future)
foreach ( Tuple < string , string > build in BuildList )
{
string Arch = build . Item1 ;
string Manifest = build . Item2 ;
string NDKArch = GetNDKArch ( Arch ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n===={Time}====PREPARING NATIVE CODE====={Arch}============================================================" , DateTime . Now . ToString ( ) , Arch ) ;
2022-05-24 19:41:41 -04:00
string UnrealBuildPath = Path . Combine ( IntermediateAndroidPath , Arch . Substring ( 1 ) . Replace ( "-" , "_" ) ) ;
// If we are packaging for Amazon then we need to copy the file to the correct location
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "bPackageDataInsideApk = {bPackageDataInsideApk}" , bPackageDataInsideApk ) ;
2022-05-24 19:41:41 -04:00
if ( bPackageDataInsideApk )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Obb location {ObbFileLocation}" , ObbFileLocation ) ;
2022-05-24 19:41:41 -04:00
string ObbFileDestination = UnrealBuildPath + "/assets" ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Obb destination location {ObbFileDestination}" , ObbFileDestination ) ;
2022-05-24 19:41:41 -04:00
if ( File . Exists ( ObbFileLocation ) )
{
Directory . CreateDirectory ( UnrealBuildPath ) ;
Directory . CreateDirectory ( ObbFileDestination ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Obb file exists..." ) ;
2022-05-24 19:41:41 -04:00
string DestFileName = Path . Combine ( ObbFileDestination , "main.obb.png" ) ; // Need a rename to turn off compression
string SrcFileName = ObbFileLocation ;
CopyIfDifferent ( SrcFileName , DestFileName , true , false ) ;
}
}
else // try to remove the file it we aren't packaging inside the APK
{
string ObbFileDestination = UnrealBuildPath + "/assets" ;
string DestFileName = Path . Combine ( ObbFileDestination , "main.obb.png" ) ;
SafeDeleteFile ( DestFileName ) ;
}
// See if we need to stage a UECommandLine.txt file in assets
string CommandLineDestFileName = Path . Combine ( UnrealBuildPath , "assets" , "UECommandLine.txt" ) ;
if ( File . Exists ( CommandLineSourceFileName ) )
{
Directory . CreateDirectory ( UnrealBuildPath ) ;
Directory . CreateDirectory ( Path . Combine ( UnrealBuildPath , "assets" ) ) ;
Console . WriteLine ( "UnrealCommandLine.txt exists..." ) ;
CopyIfDifferent ( CommandLineSourceFileName , CommandLineDestFileName , true , true ) ;
}
else // try to remove the file if we aren't packaging one
{
SafeDeleteFile ( CommandLineDestFileName ) ;
}
//Copy build files to the intermediate folder in this order (later overrides earlier):
// - Shared Engine
// - Shared Engine NoRedist (for Epic secret files)
// - Game
// - Game NoRedist (for Epic secret files)
CopyFileDirectory ( UnrealBuildFilesPath , UnrealBuildPath , Replacements ) ;
CopyFileDirectory ( UnrealBuildFilesPath_NFL , UnrealBuildPath , Replacements ) ;
CopyFileDirectory ( UnrealBuildFilesPath_NR , UnrealBuildPath , Replacements ) ;
CopyFileDirectory ( GameBuildFilesPath , UnrealBuildPath , Replacements ) ;
CopyFileDirectory ( GameBuildFilesPath_NFL , UnrealBuildPath , Replacements ) ;
CopyFileDirectory ( GameBuildFilesPath_NR , UnrealBuildPath , Replacements ) ;
// Parse Gradle filters (may have been replaced by above copies)
ParseFilterFile ( Path . Combine ( UnrealBuildPath , "GradleFilter.txt" ) ) ;
//Generate Gradle AAR dependencies
GenerateGradleAARImports ( EngineDirectory , UnrealBuildPath , NDKArches ) ;
//Now validate GooglePlay app_id if enabled
ValidateGooglePlay ( UnrealBuildPath ) ;
//determine which orientation requirements this app has
bool bNeedLandscape = false ;
bool bNeedPortrait = false ;
DetermineScreenOrientationRequirements ( NDKArches [ 0 ] , out bNeedPortrait , out bNeedLandscape ) ;
//Now keep the splash screen images matching orientation requested
PickSplashScreenOrientation ( UnrealBuildPath , bNeedPortrait , bNeedLandscape ) ;
//Now package the app based on Daydream packaging settings
PackageForDaydream ( UnrealBuildPath ) ;
//Similarly, keep only the downloader screen image matching the orientation requested
PickDownloaderScreenOrientation ( UnrealBuildPath , bNeedPortrait , bNeedLandscape ) ;
// use Gradle for compile/package
string UnrealBuildGradlePath = Path . Combine ( UnrealBuildPath , "gradle" ) ;
string UnrealBuildGradleAppPath = Path . Combine ( UnrealBuildGradlePath , "app" ) ;
string UnrealBuildGradleMainPath = Path . Combine ( UnrealBuildGradleAppPath , "src" , "main" ) ;
string CompileSDKVersion = SDKAPILevel . Replace ( "android-" , "" ) ;
// Write the manifest to the correct locations (cache and real)
String ManifestFile = Path . Combine ( IntermediateAndroidPath , Arch + "_AndroidManifest.xml" ) ;
File . WriteAllText ( ManifestFile , Manifest ) ;
ManifestFile = Path . Combine ( UnrealBuildPath , "AndroidManifest.xml" ) ;
File . WriteAllText ( ManifestFile , Manifest ) ;
// copy prebuild plugin files
UPL . ProcessPluginNode ( NDKArch , "prebuildCopies" , "" ) ;
XDocument AdditionalBuildPathFilesDoc = new XDocument ( new XElement ( "files" ) ) ;
UPL . ProcessPluginNode ( NDKArch , "additionalBuildPathFiles" , "" , ref AdditionalBuildPathFilesDoc ) ;
// Generate the OBBData.java file since different architectures may have different store version
UnrealOBBDataFileName = GetUnrealJavaOBBDataFileName ( Path . Combine ( UnrealBuildPath , "src" , PackageName . Replace ( '.' , Path . DirectorySeparatorChar ) ) ) ;
WriteJavaOBBDataFile ( UnrealOBBDataFileName , PackageName , RequiredOBBFiles , CookFlavor , bPackageDataInsideApk , Arch ) ;
// update GameActivity.java and GameApplication.java if out of date
UpdateGameActivity ( Arch , NDKArch , EngineDirectory , UnrealBuildPath ) ;
UpdateGameApplication ( Arch , NDKArch , EngineDirectory , UnrealBuildPath ) ;
// we don't actually need the SO for the bSkipGradleBuild case
string? FinalSOName = null ;
string DestApkDirectory = Path . Combine ( ProjectDirectory , "Binaries/Android" ) ;
string? DestApkName = null ;
if ( bSkipGradleBuild )
{
FinalSOName = OutputPath ;
if ( ! File . Exists ( FinalSOName ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogWarning ( "Did not find compiled .so [{FinalSOName}]" , FinalSOName ) ;
2022-05-24 19:41:41 -04:00
}
}
else
{
string SourceSOName = AndroidToolChain . InlineArchName ( OutputPath , Arch ) ;
// if the source binary was UnrealGame, replace it with the new project name, when re-packaging a binary only build
string ApkFilename = Path . GetFileNameWithoutExtension ( OutputPath ) . Replace ( "UnrealGame" , ProjectName ) ;
DestApkName = Path . Combine ( DestApkDirectory , ApkFilename + ".apk" ) ;
// As we are always making seperate APKs we need to put the architecture into the name
DestApkName = AndroidToolChain . InlineArchName ( DestApkName , Arch ) ;
if ( ! File . Exists ( SourceSOName ) )
{
throw new BuildException ( "Can't make an APK without the compiled .so [{0}]" , SourceSOName ) ;
}
if ( ! Directory . Exists ( UnrealBuildPath + "/jni" ) )
{
throw new BuildException ( "Can't make an APK without the jni directory [{0}/jni]" , UnrealBuildFilesPath ) ;
}
string JniDir = UnrealBuildPath + "/jni/" + NDKArch ;
FinalSOName = JniDir + "/libUnreal.so" ;
// clear out libs directory like ndk-build would have
string LibsDir = Path . Combine ( UnrealBuildPath , "libs" ) ;
2022-05-25 19:55:37 -04:00
DeleteDirectory ( LibsDir , Logger ) ;
2022-05-24 19:41:41 -04:00
MakeDirectoryIfRequired ( LibsDir ) ;
// check to see if libUnreal.so needs to be copied
if ( BuildListComboTotal > 1 | | FilesAreDifferent ( SourceSOName , FinalSOName ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\nCopying new .so {SourceSOName} file to jni folder..." , SourceSOName ) ;
2022-05-24 19:41:41 -04:00
Directory . CreateDirectory ( JniDir ) ;
// copy the binary to the standard .so location
File . Copy ( SourceSOName , FinalSOName , true ) ;
File . SetLastWriteTimeUtc ( FinalSOName , File . GetLastWriteTimeUtc ( SourceSOName ) ) ;
}
// remove any read only flags
FileInfo DestFileInfo = new FileInfo ( FinalSOName ) ;
DestFileInfo . Attributes = DestFileInfo . Attributes & ~ FileAttributes . ReadOnly ;
File . SetLastWriteTimeUtc ( FinalSOName , File . GetLastWriteTimeUtc ( SourceSOName ) ) ;
}
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
bool bSkipLibCpp = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bSkipLibCpp" , out bSkipLibCpp ) ;
if ( ! bSkipLibCpp )
{
// after ndk-build is called, we can now copy in the stl .so (ndk-build deletes old files)
// copy libc++_shared.so to library
CopySTL ( ToolChain , UnrealBuildPath , Arch , NDKArch , bForDistribution ) ;
}
CopyGfxDebugger ( UnrealBuildPath , Arch , NDKArch ) ;
CopyVulkanValidationLayers ( UnrealBuildPath , Arch , NDKArch , Configuration . ToString ( ) ) ;
if ( Sanitizer ! = AndroidToolChain . ClangSanitizer . None & & Sanitizer ! = AndroidToolChain . ClangSanitizer . HwAddress )
{
CopyClangSanitizerLib ( UnrealBuildPath , Arch , NDKArch , Sanitizer ) ;
}
// copy postbuild plugin files
UPL . ProcessPluginNode ( NDKArch , "resourceCopies" , "" ) ;
CreateAdditonalBuildPathFiles ( NDKArch , UnrealBuildPath , AdditionalBuildPathFilesDoc ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n===={Time}====PERFORMING FINAL APK PACKAGE OPERATION====={Arch}===========================================" , DateTime . Now . ToString ( ) , Arch ) ;
2022-05-24 19:41:41 -04:00
// check if any plugins want to increase the required compile SDK version
string CompileSDKMin = UPL . ProcessPluginNode ( NDKArch , "minimumSDKAPI" , "" ) ;
if ( CompileSDKMin ! = "" )
{
int CompileSDKVersionInt ;
if ( ! int . TryParse ( CompileSDKVersion , out CompileSDKVersionInt ) )
{
CompileSDKVersionInt = 23 ;
}
bool bUpdatedCompileSDK = false ;
string [ ] CompileSDKLines = CompileSDKMin . Split ( new [ ] { "\r\n" , "\r" , "\n" } , StringSplitOptions . None ) ;
foreach ( string CompileLine in CompileSDKLines )
{
//string VersionString = CompileLine.Replace("android-", "");
int VersionInt ;
if ( int . TryParse ( CompileLine , out VersionInt ) )
{
if ( VersionInt > CompileSDKVersionInt )
{
CompileSDKVersionInt = VersionInt ;
bUpdatedCompileSDK = true ;
}
}
}
if ( bUpdatedCompileSDK )
{
CompileSDKVersion = CompileSDKVersionInt . ToString ( ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Building Java with SDK API Level 'android-{CompileSDKVersion}' due to enabled plugin requirements" , CompileSDKVersion ) ;
2022-05-24 19:41:41 -04:00
}
}
// stage files into gradle app directory
string GradleManifest = Path . Combine ( UnrealBuildGradleMainPath , "AndroidManifest.xml" ) ;
MakeDirectoryIfRequired ( GradleManifest ) ;
CopyIfDifferent ( Path . Combine ( UnrealBuildPath , "AndroidManifest.xml" ) , GradleManifest , true , true ) ;
string [ ] Excludes ;
switch ( NDKArch )
{
default :
case "arm64-v8a" :
Excludes = new string [ ] { "armeabi-v7a" , "x86" , "x86-64" } ;
break ;
case "x86_64" :
Excludes = new string [ ] { "armeabi-v7a" , "arm64-v8a" , "x86" } ;
break ;
}
CleanCopyDirectory ( Path . Combine ( UnrealBuildPath , "jni" ) , Path . Combine ( UnrealBuildGradleMainPath , "jniLibs" ) , Excludes ) ; // has debug symbols
CleanCopyDirectory ( Path . Combine ( UnrealBuildPath , "libs" ) , Path . Combine ( UnrealBuildGradleMainPath , "libs" ) , Excludes ) ;
if ( Sanitizer ! = AndroidToolChain . ClangSanitizer . None & & Sanitizer ! = AndroidToolChain . ClangSanitizer . HwAddress )
{
CleanCopyDirectory ( Path . Combine ( UnrealBuildPath , "resources" ) , Path . Combine ( UnrealBuildGradleMainPath , "resources" ) , Excludes ) ;
}
CleanCopyDirectory ( Path . Combine ( UnrealBuildPath , "assets" ) , Path . Combine ( UnrealBuildGradleMainPath , "assets" ) ) ;
CleanCopyDirectory ( Path . Combine ( UnrealBuildPath , "res" ) , Path . Combine ( UnrealBuildGradleMainPath , "res" ) ) ;
CleanCopyDirectory ( Path . Combine ( UnrealBuildPath , "src" ) , Path . Combine ( UnrealBuildGradleMainPath , "java" ) ) ;
// do any plugin requested copies
UPL . ProcessPluginNode ( NDKArch , "gradleCopies" , "" ) ;
// get min and target SDK versions
int MinSDKVersion = 0 ;
int TargetSDKVersion = 0 ;
int NDKLevelInt = 0 ;
GetMinTargetSDKVersions ( ToolChain , Arch , UPL , NDKArch , bEnableBundle , out MinSDKVersion , out TargetSDKVersion , out NDKLevelInt ) ;
// move JavaLibs into subprojects
string JavaLibsDir = Path . Combine ( UnrealBuildPath , "JavaLibs" ) ;
PrepareJavaLibsForGradle ( JavaLibsDir , UnrealBuildGradlePath , MinSDKVersion . ToString ( ) , TargetSDKVersion . ToString ( ) , CompileSDKVersion , BuildToolsVersion , NDKArch ) ;
// Create local.properties
String LocalPropertiesFilename = Path . Combine ( UnrealBuildGradlePath , "local.properties" ) ;
StringBuilder LocalProperties = new StringBuilder ( ) ;
LocalProperties . AppendLine ( string . Format ( "ndk.dir={0}" , Environment . GetEnvironmentVariable ( "NDKROOT" ) ! . Replace ( "\\" , "/" ) ) ) ;
LocalProperties . AppendLine ( string . Format ( "sdk.dir={0}" , Environment . GetEnvironmentVariable ( "ANDROID_HOME" ) ! . Replace ( "\\" , "/" ) ) ) ;
File . WriteAllText ( LocalPropertiesFilename , LocalProperties . ToString ( ) ) ;
CreateGradlePropertiesFiles ( Arch , MinSDKVersion , TargetSDKVersion , CompileSDKVersion , BuildToolsVersion , PackageName , DestApkName , NDKArch ,
UnrealBuildFilesPath , GameBuildFilesPath , UnrealBuildGradleAppPath , UnrealBuildPath , UnrealBuildGradlePath , bForDistribution , bSkipGradleBuild , RequiredOBBFiles ) ;
if ( ! bSkipGradleBuild )
{
string GradleScriptPath = Path . Combine ( UnrealBuildGradlePath , "gradlew" ) ;
if ( ! RuntimePlatform . IsWindows )
{
// fix permissions for Mac/Linux
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithException ( UnrealBuildGradlePath , "/bin/sh" , string . Format ( "-c 'chmod 0755 \"{0}\"'" , GradleScriptPath . Replace ( "'" , "'\"'\"'" ) ) , Logger , "Fix gradlew permissions" ) ;
2022-05-24 19:41:41 -04:00
}
else
{
if ( CreateRunGradle ( UnrealBuildGradlePath ) )
{
GradleScriptPath = Path . Combine ( UnrealBuildGradlePath , "rungradle.bat" ) ;
}
else
{
GradleScriptPath = Path . Combine ( UnrealBuildGradlePath , "gradlew.bat" ) ;
}
}
if ( ! bEnableBundle )
{
string GradleBuildType = bForDistribution ? ":app:assembleRelease" : ":app:assembleDebug" ;
// collect optional additional Gradle parameters from plugins
string GradleOptions = UPL . ProcessPluginNode ( NDKArch , "gradleParameters" , GradleBuildType ) ; // "--stacktrace --debug " + GradleBuildType);
string GradleSecondCallOptions = UPL . ProcessPluginNode ( NDKArch , "gradleSecondCallParameters" , "" ) ;
// check for Android Studio project, call Gradle if doesn't exist (assume user will build with Android Studio)
string GradleAppImlFilename = Path . Combine ( UnrealBuildGradlePath , "app.iml" ) ;
if ( ! File . Exists ( GradleAppImlFilename ) )
{
// make sure destination exists
Directory . CreateDirectory ( Path . GetDirectoryName ( DestApkName ) ! ) ;
// Use gradle to build the .apk file
string ShellExecutable = RuntimePlatform . IsWindows ? "cmd.exe" : "/bin/sh" ;
string ShellParametersBegin = RuntimePlatform . IsWindows ? "/c " : "-c '" ;
string ShellParametersEnd = RuntimePlatform . IsWindows ? "" : "'" ;
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithExceptionAndFiltering ( UnrealBuildGradlePath , ShellExecutable , ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd , Logger , "Making .apk with Gradle..." ) ;
2022-05-24 19:41:41 -04:00
if ( GradleSecondCallOptions ! = "" )
{
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithExceptionAndFiltering ( UnrealBuildGradlePath , ShellExecutable , ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleSecondCallOptions + ShellParametersEnd , Logger , "Additional Gradle steps..." ) ;
2022-05-24 19:41:41 -04:00
}
// For build machine run a clean afterward to clean up intermediate files (does not remove final APK)
if ( bIsBuildMachine )
{
//GradleOptions = "tasks --all";
//RunCommandLineProgramWithException(UnrealBuildGradlePath, ShellExecutable, ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd, "Listing all tasks...");
GradleOptions = "clean" ;
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithExceptionAndFiltering ( UnrealBuildGradlePath , ShellExecutable , ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd , Logger , "Cleaning Gradle intermediates..." ) ;
2022-05-24 19:41:41 -04:00
}
}
else
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "=============================================================================================" ) ;
Logger . LogInformation ( "Android Studio project found, skipping Gradle; complete creation of APK in Android Studio!!!!" ) ;
Logger . LogInformation ( "Delete '{GradleAppImlFilename} if you want to have UnrealBuildTool run Gradle for future runs." , GradleAppImlFilename ) ;
Logger . LogInformation ( "=============================================================================================" ) ;
2022-05-24 19:41:41 -04:00
}
}
}
bool bBuildWithHiddenSymbolVisibility = false ;
bool bSaveSymbols = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bBuildWithHiddenSymbolVisibility" , out bBuildWithHiddenSymbolVisibility ) ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bSaveSymbols" , out bSaveSymbols ) ;
bSaveSymbols = true ;
if ( bSaveSymbols | | ( Configuration = = UnrealTargetConfiguration . Shipping & & bBuildWithHiddenSymbolVisibility ) )
{
// Copy .so with symbols to
int StoreVersion = GetStoreVersion ( Arch ) ;
string SymbolSODirectory = Path . Combine ( DestApkDirectory , ProjectName + "_Symbols_v" + StoreVersion + "/" + ProjectName + Arch ) ;
string SymbolifiedSOPath = Path . Combine ( SymbolSODirectory , Path . GetFileName ( FinalSOName ) ) ;
MakeDirectoryIfRequired ( SymbolifiedSOPath ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Writing symbols to {SymbolifiedSOPath}" , SymbolifiedSOPath ) ;
2022-05-24 19:41:41 -04:00
File . Copy ( FinalSOName , SymbolifiedSOPath , true ) ;
}
}
// Deal with generating an App Bundle
if ( bEnableBundle & & ! bSkipGradleBuild )
{
bool bCombinedBundleOK = true ;
// try to make a combined Gradle project for all architectures (may fail if incompatible configuration)
string UnrealGradleDest = Path . Combine ( IntermediateAndroidPath , "gradle" ) ;
// start fresh each time for now
2022-05-25 19:55:37 -04:00
DeleteDirectory ( UnrealGradleDest , Logger ) ;
2022-05-24 19:41:41 -04:00
// make sure destination exists
Directory . CreateDirectory ( UnrealGradleDest ) ;
String ABIFilter = "" ;
// loop through and merge the different architecture gradle directories
foreach ( Tuple < string , string > build in BuildList )
{
string Arch = build . Item1 ;
string Manifest = build . Item2 ;
string NDKArch = GetNDKArch ( Arch ) ;
string UnrealBuildPath = Path . Combine ( IntermediateAndroidPath , Arch . Substring ( 1 ) . Replace ( "-" , "_" ) ) ;
string UnrealBuildGradlePath = Path . Combine ( UnrealBuildPath , "gradle" ) ;
if ( ! Directory . Exists ( UnrealBuildGradlePath ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Source directory missing: {GradlePath}" , UnrealBuildGradlePath ) ;
2022-05-24 19:41:41 -04:00
bCombinedBundleOK = false ;
break ;
}
ABIFilter + = ", \"" + NDKArch + "\"" ;
string [ ] SourceFiles = Directory . GetFiles ( UnrealBuildGradlePath , "*.*" , SearchOption . AllDirectories ) ;
foreach ( string Filename in SourceFiles )
{
// make the dest filename with the same structure as it was in SourceDir
string DestFilename = Path . Combine ( UnrealGradleDest , Utils . MakePathRelativeTo ( Filename , UnrealBuildGradlePath ) ) ;
// skip the build directories
string Workname = Filename . Replace ( "\\" , "/" ) ;
string DirectoryName = Path . GetDirectoryName ( Filename ) ! ;
if ( DirectoryName . Contains ( "build" ) | | Workname . Contains ( "/." ) )
{
continue ;
}
// if destination doesn't exist, just copy it
if ( ! File . Exists ( DestFilename ) )
{
string DestSubdir = Path . GetDirectoryName ( DestFilename ) ! ;
if ( ! Directory . Exists ( DestSubdir ) )
{
Directory . CreateDirectory ( DestSubdir ) ;
}
// copy it
File . Copy ( Filename , DestFilename ) ;
// preserve timestamp and clear read-only flags
FileInfo DestFileInfo = new FileInfo ( DestFilename ) ;
DestFileInfo . Attributes = DestFileInfo . Attributes & ~ FileAttributes . ReadOnly ;
File . SetLastWriteTimeUtc ( DestFilename , File . GetLastWriteTimeUtc ( Filename ) ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Copied file {DestFilename}." , DestFilename ) ;
2022-05-24 19:41:41 -04:00
continue ;
}
if ( FilesAreIdentical ( Filename , DestFilename ) )
{
continue ;
}
// ignore abi.gradle, we're going to generate a new one
if ( Filename . EndsWith ( "abi.gradle" ) )
{
continue ;
}
// ignore OBBData.java, we won't use it
if ( Filename . EndsWith ( "OBBData.java" ) )
{
continue ;
}
// deal with AndroidManifest.xml
if ( Filename . EndsWith ( "AndroidManifest.xml" ) )
{
// only allowed to differ by versionCode
string [ ] SourceManifest = File . ReadAllLines ( Filename ) ;
string [ ] DestManifest = File . ReadAllLines ( DestFilename ) ;
if ( SourceManifest . Length = = DestManifest . Length )
{
bool bDiffers = false ;
for ( int Index = 0 ; Index < SourceManifest . Length ; Index + + )
{
if ( SourceManifest [ Index ] = = DestManifest [ Index ] )
{
continue ;
}
int SourceVersionIndex = SourceManifest [ Index ] . IndexOf ( "android:versionCode=" ) ;
if ( SourceVersionIndex < 0 )
{
bDiffers = true ;
break ;
}
int DestVersionIndex = DestManifest [ Index ] . IndexOf ( "android:versionCode=" ) ;
if ( DestVersionIndex < 0 )
{
bDiffers = true ;
break ;
}
int SourceVersionIndex2 = SourceManifest [ Index ] . Substring ( SourceVersionIndex + 22 ) . IndexOf ( "\"" ) ;
string FixedSource = SourceManifest [ Index ] . Substring ( 0 , SourceVersionIndex + 21 ) + SourceManifest [ Index ] . Substring ( SourceVersionIndex + 22 + SourceVersionIndex2 ) ;
int DestVersionIndex2 = DestManifest [ Index ] . Substring ( DestVersionIndex + 22 ) . IndexOf ( "\"" ) ;
string FixedDest = SourceManifest [ Index ] . Substring ( 0 , DestVersionIndex + 21 ) + DestManifest [ Index ] . Substring ( DestVersionIndex + 22 + DestVersionIndex2 ) ;
if ( FixedSource ! = FixedDest )
{
bDiffers = true ;
break ;
}
}
if ( ! bDiffers )
{
continue ;
}
}
// differed too much
bCombinedBundleOK = false ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "AndroidManifest.xml files differ too much to combine for single AAB: '{Filename}' != '{DestFilename}'" , Filename , DestFilename ) ;
2022-05-24 19:41:41 -04:00
break ;
}
// deal with buildAdditions.gradle
if ( Filename . EndsWith ( "buildAdditions.gradle" ) )
{
// allow store filepath to differ
string [ ] SourceProperties = File . ReadAllLines ( Filename ) ;
string [ ] DestProperties = File . ReadAllLines ( DestFilename ) ;
if ( SourceProperties . Length = = DestProperties . Length )
{
bool bDiffers = false ;
for ( int Index = 0 ; Index < SourceProperties . Length ; Index + + )
{
if ( SourceProperties [ Index ] = = DestProperties [ Index ] )
{
continue ;
}
if ( SourceProperties [ Index ] . Contains ( "storeFile file(" ) & & DestProperties [ Index ] . Contains ( "storeFile file(" ) )
{
continue ;
}
bDiffers = true ;
break ;
}
if ( ! bDiffers )
{
continue ;
}
}
// differed too much
bCombinedBundleOK = false ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "buildAdditions.gradle files differ too much to combine for single AAB: '{Filename}' != '{DestFilename}'" , Filename , DestFilename ) ;
2022-05-24 19:41:41 -04:00
break ;
}
// deal with gradle.properties
if ( Filename . EndsWith ( "gradle.properties" ) )
{
// allow STORE_VERSION and OUTPUT_FILENAME to differ
// only allowed to differ by versionCode
string [ ] SourceProperties = File . ReadAllLines ( Filename ) ;
string [ ] DestProperties = File . ReadAllLines ( DestFilename ) ;
if ( SourceProperties . Length = = DestProperties . Length )
{
bool bDiffers = false ;
for ( int Index = 0 ; Index < SourceProperties . Length ; Index + + )
{
if ( SourceProperties [ Index ] = = DestProperties [ Index ] )
{
continue ;
}
if ( SourceProperties [ Index ] . StartsWith ( "STORE_VERSION=" ) & & DestProperties [ Index ] . StartsWith ( "STORE_VERSION=" ) )
{
continue ;
}
if ( SourceProperties [ Index ] . StartsWith ( "STORE_FILE=" ) & & DestProperties [ Index ] . StartsWith ( "STORE_FILE=" ) )
{
continue ;
}
if ( SourceProperties [ Index ] . StartsWith ( "OUTPUT_FILENAME=" ) & & DestProperties [ Index ] . StartsWith ( "OUTPUT_FILENAME=" ) )
{
continue ;
}
if ( SourceProperties [ Index ] . StartsWith ( "OUTPUT_BUNDLEFILENAME=" ) & & DestProperties [ Index ] . StartsWith ( "OUTPUT_BUNDLEFILENAME=" ) )
{
continue ;
}
if ( SourceProperties [ Index ] . StartsWith ( "OUTPUT_UNIVERSALFILENAME=" ) & & DestProperties [ Index ] . StartsWith ( "OUTPUT_UNIVERSALFILENAME=" ) )
{
continue ;
}
bDiffers = true ;
break ;
}
if ( ! bDiffers )
{
continue ;
}
}
// differed too much
bCombinedBundleOK = false ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "gradle.properties files differ too much to combine for single AAB: '{Filename}' != '{DestFilename}'" , Filename , DestFilename ) ;
2022-05-24 19:41:41 -04:00
break ;
}
// there are unknown differences, cannot make a single AAB
bCombinedBundleOK = false ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Gradle projects differ too much to combine for single AAB: '{Filename}' != '{DestFilename}'" , Filename , DestFilename ) ;
2022-05-24 19:41:41 -04:00
break ;
}
}
if ( bCombinedBundleOK )
{
string NDKArch = NDKArches [ 0 ] ;
string UnrealBuildGradlePath = UnrealGradleDest ;
// write a new abi.gradle
StringBuilder ABIGradle = new StringBuilder ( ) ;
ABIGradle . AppendLine ( "android {" ) ;
ABIGradle . AppendLine ( "\tdefaultConfig {" ) ;
ABIGradle . AppendLine ( "\t\tndk {" ) ;
ABIGradle . AppendLine ( string . Format ( "\t\t\tabiFilters{0}" , ABIFilter . Substring ( 1 ) ) ) ;
ABIGradle . AppendLine ( "\t\t}" ) ;
ABIGradle . AppendLine ( "\t}" ) ;
ABIGradle . AppendLine ( "}" ) ;
string ABIGradleFilename = Path . Combine ( UnrealGradleDest , "app" , "abi.gradle" ) ;
File . WriteAllText ( ABIGradleFilename , ABIGradle . ToString ( ) ) ;
// update manifest to use versionCode properly
string BaseStoreVersion = GetStoreVersion ( "default" ) . ToString ( ) ;
string ManifestFilename = Path . Combine ( UnrealBuildGradlePath , "app" , "src" , "main" , "AndroidManifest.xml" ) ;
string [ ] ManifestContents = File . ReadAllLines ( ManifestFilename ) ;
for ( int Index = 0 ; Index < ManifestContents . Length ; Index + + )
{
int ManifestVersionIndex = ManifestContents [ Index ] . IndexOf ( "android:versionCode=" ) ;
if ( ManifestVersionIndex < 0 )
{
continue ;
}
int ManifestVersionIndex2 = ManifestContents [ Index ] . Substring ( ManifestVersionIndex + 22 ) . IndexOf ( "\"" ) ;
ManifestContents [ Index ] = ManifestContents [ Index ] . Substring ( 0 , ManifestVersionIndex + 21 ) + BaseStoreVersion + ManifestContents [ Index ] . Substring ( ManifestVersionIndex + 22 + ManifestVersionIndex2 ) ;
break ;
}
File . WriteAllLines ( ManifestFilename , ManifestContents ) ;
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
bool bEnableUniversalAPK = false ;
Ini . GetBool ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "bEnableUniversalAPK" , out bEnableUniversalAPK ) ;
// update gradle.properties to set STORE_VERSION properly, and OUTPUT_BUNDLEFILENAME
string GradlePropertiesFilename = Path . Combine ( UnrealBuildGradlePath , "gradle.properties" ) ;
string GradlePropertiesContent = File . ReadAllText ( GradlePropertiesFilename ) ;
GradlePropertiesContent + = string . Format ( "\nSTORE_VERSION={0}\nOUTPUT_BUNDLEFILENAME={1}\n" , BaseStoreVersion ,
Path . GetFileNameWithoutExtension ( OutputPath ) . Replace ( "UnrealGame" , ProjectName ) + ".aab" ) ;
if ( bEnableUniversalAPK )
{
GradlePropertiesContent + = string . Format ( "OUTPUT_UNIVERSALFILENAME={0}\n" ,
Path . GetFileNameWithoutExtension ( OutputPath ) . Replace ( "UnrealGame" , ProjectName ) + "_universal.apk" ) ;
}
File . WriteAllText ( GradlePropertiesFilename , GradlePropertiesContent ) ;
string GradleScriptPath = Path . Combine ( UnrealBuildGradlePath , "gradlew" ) ;
if ( ! RuntimePlatform . IsWindows )
{
// fix permissions for Mac/Linux
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithException ( UnrealBuildGradlePath , "/bin/sh" , string . Format ( "-c 'chmod 0755 \"{0}\"'" , GradleScriptPath . Replace ( "'" , "'\"'\"'" ) ) , Logger , "Fix gradlew permissions" ) ;
2022-05-24 19:41:41 -04:00
}
else
{
if ( CreateRunGradle ( UnrealBuildGradlePath ) )
{
GradleScriptPath = Path . Combine ( UnrealBuildGradlePath , "rungradle.bat" ) ;
}
else
{
GradleScriptPath = Path . Combine ( UnrealBuildGradlePath , "gradlew.bat" ) ;
}
}
string GradleBuildType = bForDistribution ? ":app:bundleRelease" : ":app:bundleDebug" ;
// collect optional additional Gradle parameters from plugins
string GradleOptions = UPL . ProcessPluginNode ( NDKArch , "gradleParameters" , GradleBuildType ) ; // "--stacktrace --debug " + GradleBuildType);
string GradleSecondCallOptions = UPL . ProcessPluginNode ( NDKArch , "gradleSecondCallParameters" , "" ) ;
// make sure destination exists
//Directory.CreateDirectory(Path.GetDirectoryName(DestApkName));
// Use gradle to build the .apk file
string ShellExecutable = RuntimePlatform . IsWindows ? "cmd.exe" : "/bin/sh" ;
string ShellParametersBegin = RuntimePlatform . IsWindows ? "/c " : "-c '" ;
string ShellParametersEnd = RuntimePlatform . IsWindows ? "" : "'" ;
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithExceptionAndFiltering ( UnrealBuildGradlePath , ShellExecutable , ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd , Logger , "Making .aab with Gradle..." ) ;
2022-05-24 19:41:41 -04:00
if ( GradleSecondCallOptions ! = "" )
{
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithExceptionAndFiltering ( UnrealBuildGradlePath , ShellExecutable , ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleSecondCallOptions + ShellParametersEnd , Logger , "Additional Gradle steps..." ) ;
2022-05-24 19:41:41 -04:00
}
// For build machine run a clean afterward to clean up intermediate files (does not remove final APK)
if ( bIsBuildMachine )
{
//GradleOptions = "tasks --all";
//RunCommandLineProgramWithException(UnrealBuildGradlePath, ShellExecutable, ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd, "Listing all tasks...");
GradleOptions = "clean" ;
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithExceptionAndFiltering ( UnrealBuildGradlePath , ShellExecutable , ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd , Logger , "Cleaning Gradle intermediates..." ) ;
2022-05-24 19:41:41 -04:00
}
}
else
{
// generate an AAB for each architecture separately, was unable to merge
foreach ( Tuple < string , string > build in BuildList )
{
string Arch = build . Item1 ;
string Manifest = build . Item2 ;
string NDKArch = GetNDKArch ( Arch ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n===={Time}====GENERATING BUNDLE====={Arch}================================================================" , DateTime . Now . ToString ( ) , Arch ) ;
2022-05-24 19:41:41 -04:00
string UnrealBuildPath = Path . Combine ( IntermediateAndroidPath , Arch . Substring ( 1 ) . Replace ( "-" , "_" ) ) ;
string UnrealBuildGradlePath = Path . Combine ( UnrealBuildPath , "gradle" ) ;
string GradleScriptPath = Path . Combine ( UnrealBuildGradlePath , "gradlew" ) ;
if ( ! RuntimePlatform . IsWindows )
{
// fix permissions for Mac/Linux
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithException ( UnrealBuildGradlePath , "/bin/sh" , string . Format ( "-c 'chmod 0755 \"{0}\"'" , GradleScriptPath . Replace ( "'" , "'\"'\"'" ) ) , Logger , "Fix gradlew permissions" ) ;
2022-05-24 19:41:41 -04:00
}
else
{
if ( CreateRunGradle ( UnrealBuildGradlePath ) )
{
GradleScriptPath = Path . Combine ( UnrealBuildGradlePath , "rungradle.bat" ) ;
}
else
{
GradleScriptPath = Path . Combine ( UnrealBuildGradlePath , "gradlew.bat" ) ;
}
}
string GradleBuildType = bForDistribution ? ":app:bundleRelease" : ":app:bundleDebug" ;
// collect optional additional Gradle parameters from plugins
string GradleOptions = UPL . ProcessPluginNode ( NDKArch , "gradleParameters" , GradleBuildType ) ; // "--stacktrace --debug " + GradleBuildType);
string GradleSecondCallOptions = UPL . ProcessPluginNode ( NDKArch , "gradleSecondCallParameters" , "" ) ;
// make sure destination exists
//Directory.CreateDirectory(Path.GetDirectoryName(DestApkName));
// Use gradle to build the .apk file
string ShellExecutable = RuntimePlatform . IsWindows ? "cmd.exe" : "/bin/sh" ;
string ShellParametersBegin = RuntimePlatform . IsWindows ? "/c " : "-c '" ;
string ShellParametersEnd = RuntimePlatform . IsWindows ? "" : "'" ;
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithExceptionAndFiltering ( UnrealBuildGradlePath , ShellExecutable , ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd , Logger , "Making .aab with Gradle..." ) ;
2022-05-24 19:41:41 -04:00
if ( GradleSecondCallOptions ! = "" )
{
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithExceptionAndFiltering ( UnrealBuildGradlePath , ShellExecutable , ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleSecondCallOptions + ShellParametersEnd , Logger , "Additional Gradle steps..." ) ;
2022-05-24 19:41:41 -04:00
}
// For build machine run a clean afterward to clean up intermediate files (does not remove final APK)
if ( bIsBuildMachine )
{
//GradleOptions = "tasks --all";
//RunCommandLineProgramWithException(UnrealBuildGradlePath, ShellExecutable, ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd, "Listing all tasks...");
GradleOptions = "clean" ;
2022-05-25 19:55:37 -04:00
RunCommandLineProgramWithExceptionAndFiltering ( UnrealBuildGradlePath , ShellExecutable , ShellParametersBegin + "\"" + GradleScriptPath + "\" " + GradleOptions + ShellParametersEnd , Logger , "Cleaning Gradle intermediates..." ) ;
2022-05-24 19:41:41 -04:00
}
}
}
}
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "\n===={Time}====COMPLETED MAKE APK=======================================================================" , DateTime . Now . ToString ( ) ) ;
2022-05-24 19:41:41 -04:00
}
private List < string > CollectPluginDataPaths ( TargetReceipt Receipt )
{
List < string > PluginExtras = new List < string > ( ) ;
if ( Receipt = = null )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Receipt is NULL" ) ;
2022-05-24 19:41:41 -04:00
return PluginExtras ;
}
// collect plugin extra data paths from target receipt
IEnumerable < ReceiptProperty > Results = Receipt . AdditionalProperties . Where ( x = > x . Name = = "AndroidPlugin" ) ;
foreach ( ReceiptProperty Property in Results )
{
// Keep only unique paths
string PluginPath = Property . Value ;
if ( PluginExtras . FirstOrDefault ( x = > x = = PluginPath ) = = null )
{
PluginExtras . Add ( PluginPath ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "AndroidPlugin: {PluginPath}" , PluginPath ) ;
2022-05-24 19:41:41 -04:00
}
}
return PluginExtras ;
}
public override bool PrepTargetForDeployment ( TargetReceipt Receipt )
{
2022-05-25 19:55:37 -04:00
//Logger.LogInformation("$$$$$$$$$$$$$$ PrepTargetForDeployment $$$$$$$$$$$$$$$$$");
2022-05-24 19:41:41 -04:00
DirectoryReference ProjectDirectory = DirectoryReference . FromFile ( Receipt . ProjectFile ) ? ? Unreal . EngineDirectory ;
string TargetName = ( Receipt . ProjectFile = = null ? Receipt . TargetName : Receipt . ProjectFile . GetFileNameWithoutAnyExtensions ( ) ) ;
AndroidToolChain ToolChain = ( AndroidToolChain ) ( ( AndroidPlatform ) UEBuildPlatform . GetBuildPlatform ( Receipt . Platform ) ) . CreateTempToolChainForProject ( Receipt . ProjectFile ) ;
// get the receipt
SetAndroidPluginData ( ToolChain . GetAllArchitectures ( ) , CollectPluginDataPaths ( Receipt ) ) ;
bool bShouldCompileAsDll = Receipt . HasValueForAdditionalProperty ( "CompileAsDll" , "true" ) ;
SavePackageInfo ( TargetName , ProjectDirectory . FullName , Receipt . TargetType , bShouldCompileAsDll ) ;
// Get the output paths
BuildProductType ProductType = bShouldCompileAsDll ? BuildProductType . DynamicLibrary : BuildProductType . Executable ;
List < FileReference > OutputPaths = Receipt . BuildProducts . Where ( x = > x . Type = = ProductType ) . Select ( x = > x . Path ) . ToList ( ) ;
if ( OutputPaths . Count < 1 )
{
throw new BuildException ( "Target file does not contain either executable or dynamic library .so" ) ;
}
// we need to strip architecture from any of the output paths
string BaseSoName = ToolChain . RemoveArchName ( OutputPaths [ 0 ] . FullName ) ;
// make an apk at the end of compiling, so that we can run without packaging (debugger, cook on the fly, etc)
string RelativeEnginePath = Unreal . EngineDirectory . MakeRelativeTo ( DirectoryReference . GetCurrentDirectory ( ) ) ;
MakeApk ( ToolChain , TargetName , Receipt . TargetType , ProjectDirectory . FullName , BaseSoName , RelativeEnginePath , bForDistribution : false , CookFlavor : "" , Configuration : Receipt . Configuration ,
bMakeSeparateApks : ShouldMakeSeparateApks ( ) , bIncrementalPackage : true , bDisallowPackagingDataInApk : false , bDisallowExternalFilesDir : true , bSkipGradleBuild : bShouldCompileAsDll ) ;
// if we made any non-standard .apk files, the generated debugger settings may be wrong
if ( ShouldMakeSeparateApks ( ) & & ( OutputPaths . Count > 1 | | ! OutputPaths [ 0 ] . FullName . Contains ( "-armv7" ) ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "================================================================================================================================" ) ;
Logger . LogInformation ( "Non-default apk(s) have been made: If you are debugging, you will need to manually select one to run in the debugger properties!" ) ;
Logger . LogInformation ( "================================================================================================================================" ) ;
2022-05-24 19:41:41 -04:00
}
return true ;
}
// Store generated package name in a text file for builds that do not generate an apk file
public bool SavePackageInfo ( string TargetName , string ProjectDirectory , TargetType InTargetType , bool bIsEmbedded )
{
string PackageName = GetPackageName ( TargetName ) ;
string DestPackageNameFileName = Path . Combine ( ProjectDirectory , "Binaries" , "Android" , "packageInfo.txt" ) ;
string [ ] PackageInfoSource = new string [ 4 ] ;
PackageInfoSource [ 0 ] = PackageName ;
PackageInfoSource [ 1 ] = GetStoreVersion ( "" ) . ToString ( ) ;
PackageInfoSource [ 2 ] = GetVersionDisplayName ( bIsEmbedded ) ;
PackageInfoSource [ 3 ] = string . Format ( "name='com.epicgames.unreal.GameActivity.AppType' value='{0}'" , InTargetType = = TargetType . Game ? "" : InTargetType . ToString ( ) ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "Writing packageInfo pkgName:{PkgName} storeVersion:{StoreVer} versionDisplayName:{Version} to {DestFile}" , PackageInfoSource [ 0 ] , PackageInfoSource [ 1 ] , PackageInfoSource [ 2 ] , DestPackageNameFileName ) ;
2022-05-24 19:41:41 -04:00
string DestDirectory = Path . GetDirectoryName ( DestPackageNameFileName ) ! ;
if ( ! Directory . Exists ( DestDirectory ) )
{
Directory . CreateDirectory ( DestDirectory ) ;
}
File . WriteAllLines ( DestPackageNameFileName , PackageInfoSource ) ;
return true ;
}
public static bool ShouldMakeSeparateApks ( )
{
// @todo android fat binary: Currently, there isn't much utility in merging multiple .so's into a single .apk except for debugging,
// but we can't properly handle multiple GPU architectures in a single .apk, so we are disabling the feature for now
// The user will need to manually select the apk to run in their Visual Studio debugger settings (see Override APK in TADP, for instance)
// If we change this, pay attention to <OverrideAPKPath> in AndroidProjectGenerator
return true ;
// check to see if the project wants separate apks
// ConfigCacheIni Ini = nGetConfigCacheIni("Engine");
// bool bSeparateApks = false;
// Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bSplitIntoSeparateApks", out bSeparateApks);
//
// return bSeparateApks;
}
public bool PrepForUATPackageOrDeploy ( FileReference ProjectFile , string ProjectName , DirectoryReference ProjectDirectory , string ExecutablePath , string EngineDirectory , bool bForDistribution , string CookFlavor , UnrealTargetConfiguration Configuration , bool bIsDataDeploy , bool bSkipGradleBuild )
{
2022-05-25 19:55:37 -04:00
//Logger.LogInformation("$$$$$$$$$$$$$$ PrepForUATPackageOrDeploy $$$$$$$$$$$$$$$$$");
2022-05-24 19:41:41 -04:00
TargetType Type = TargetType . Game ;
if ( CookFlavor . EndsWith ( "Client" ) )
{
Type = TargetType . Client ;
}
else if ( CookFlavor . EndsWith ( "Server" ) )
{
Type = TargetType . Server ;
}
// note that we cannot allow the data packaged into the APK if we are doing something like Launch On that will not make an obb
// file and instead pushes files directly via deploy
AndroidTargetRules TargetRules = new AndroidTargetRules ( ) ;
2022-05-25 19:55:37 -04:00
CommandLine . ParseArguments ( Environment . GetCommandLineArgs ( ) , TargetRules , Logger ) ;
2022-05-24 19:41:41 -04:00
ClangToolChainOptions Options = AndroidPlatform . CreateToolChainOptions ( TargetRules ) ;
2022-05-25 19:55:37 -04:00
AndroidToolChain ToolChain = new AndroidToolChain ( ProjectFile , false , null , null , Options , Logger ) ;
2022-05-24 19:41:41 -04:00
SavePackageInfo ( ProjectName , ProjectDirectory . FullName , Type , bSkipGradleBuild ) ;
MakeApk ( ToolChain , ProjectName , Type , ProjectDirectory . FullName , ExecutablePath , EngineDirectory , bForDistribution : bForDistribution , CookFlavor : CookFlavor , Configuration : Configuration ,
bMakeSeparateApks : ShouldMakeSeparateApks ( ) , bIncrementalPackage : false , bDisallowPackagingDataInApk : bIsDataDeploy , bDisallowExternalFilesDir : ! bForDistribution | | bIsDataDeploy , bSkipGradleBuild : bSkipGradleBuild ) ;
return true ;
}
2022-05-25 19:55:37 -04:00
public static void OutputReceivedDataEventHandler ( Object Sender , DataReceivedEventArgs Line , ILogger Logger )
2022-05-24 19:41:41 -04:00
{
if ( ( Line ! = null ) & & ( Line . Data ! = null ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "{Output}" , Line . Data ) ;
2022-05-24 19:41:41 -04:00
}
}
private string GenerateTemplatesHashCode ( string EngineDir )
{
string SourceDirectory = Path . Combine ( EngineDir , "Build" , "Android" , "Java" ) ;
if ( ! Directory . Exists ( SourceDirectory ) )
{
return "badpath" ;
}
MD5 md5 = MD5 . Create ( ) ;
byte [ ] ? TotalHashBytes = null ;
string [ ] SourceFiles = Directory . GetFiles ( SourceDirectory , "*.*" , SearchOption . AllDirectories ) ;
foreach ( string Filename in SourceFiles )
{
using ( FileStream stream = File . OpenRead ( Filename ) )
{
byte [ ] FileHashBytes = md5 . ComputeHash ( stream ) ;
if ( TotalHashBytes ! = null )
{
int index = 0 ;
foreach ( byte b in FileHashBytes )
{
TotalHashBytes [ index ] ^ = b ;
index + + ;
}
}
else
{
TotalHashBytes = FileHashBytes ;
}
}
}
if ( TotalHashBytes ! = null )
{
string HashCode = "" ;
foreach ( byte b in TotalHashBytes )
{
HashCode + = b . ToString ( "x2" ) ;
}
return HashCode ;
}
return "empty" ;
}
private void UpdateGameActivity ( string UnrealArch , string NDKArch , string EngineDir , string UnrealBuildPath )
{
string SourceFilename = Path . Combine ( EngineDir , "Build" , "Android" , "Java" , "src" , "com" , "epicgames" , "unreal" , "GameActivity.java.template" ) ;
string DestFilename = Path . Combine ( UnrealBuildPath , "src" , "com" , "epicgames" , "unreal" , "GameActivity.java" ) ;
// check for GameActivity.java.template override
SourceFilename = UPL ! . ProcessPluginNode ( NDKArch , "gameActivityReplacement" , SourceFilename ) ;
ConfigHierarchy Ini = GetConfigCacheIni ( ConfigHierarchyType . Engine ) ;
string LoadLibraryDefaults = "" ;
string SuperClassDefault ;
if ( ! Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "GameActivitySuperClass" , out SuperClassDefault ) )
{
SuperClassDefault = UPL . ProcessPluginNode ( NDKArch , "gameActivitySuperClass" , "" ) ;
if ( String . IsNullOrEmpty ( SuperClassDefault ) )
{
SuperClassDefault = "NativeActivity" ;
}
}
string AndroidGraphicsDebugger ;
Ini . GetString ( "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings" , "AndroidGraphicsDebugger" , out AndroidGraphicsDebugger ) ;
switch ( AndroidGraphicsDebugger . ToLower ( ) )
{
case "mali" :
LoadLibraryDefaults + = "\t\ttry\n" +
"\t\t{\n" +
"\t\t\tSystem.loadLibrary(\"MGD\");\n" +
"\t\t}\n" +
"\t\tcatch (java.lang.UnsatisfiedLinkError e)\n" +
"\t\t{\n" +
"\t\t\tLog.debug(\"libMGD.so not loaded.\");\n" +
"\t\t}\n" ;
break ;
}
Dictionary < string , string > Replacements = new Dictionary < string , string > {
{ "//$${gameActivityImportAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityImportAdditions" , "" ) } ,
{ "//$${gameActivityPostImportAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityPostImportAdditions" , "" ) } ,
{ "//$${gameActivityImplementsAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityImplementsAdditions" , "" ) } ,
{ "//$${gameActivityClassAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityClassAdditions" , "" ) } ,
{ "//$${gameActivityReadMetadataAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityReadMetadataAdditions" , "" ) } ,
{ "//$${gameActivityOnCreateBeginningAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnCreateBeginningAdditions" , "" ) } ,
{ "//$${gameActivityOnCreateAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnCreateAdditions" , "" ) } ,
{ "//$${gameActivityOnCreateFinalAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnCreateFinalAdditions" , "" ) } ,
{ "//$${gameActivityOverrideAPKOBBPackaging}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOverrideAPKOBBPackaging" , "" ) } ,
{ "//$${gameActivityOnDestroyAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnDestroyAdditions" , "" ) } ,
{ "//$${gameActivityonConfigurationChangedAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityonConfigurationChangedAdditions" , "" ) } ,
{ "//$${gameActivityOnStartAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnStartAdditions" , "" ) } ,
{ "//$${gameActivityOnStopAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnStopAdditions" , "" ) } ,
{ "//$${gameActivityOnRestartAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnRestartAdditions" , "" ) } ,
{ "//$${gameActivityOnSaveInstanceStateAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnSaveInstanceStateAdditions" , "" ) } ,
{ "//$${gameActivityOnRequestPermissionsResultAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnRequestPermissionsResultAdditions" , "" ) } ,
{ "//$${gameActivityOnPauseAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnPauseAdditions" , "" ) } ,
{ "//$${gameActivityOnResumeAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnResumeAdditions" , "" ) } ,
{ "//$${gameActivityOnNewIntentAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnNewIntentAdditions" , "" ) } ,
{ "//$${gameActivityOnActivityResultAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnActivityResultAdditions" , "" ) } ,
{ "//$${gameActivityOnActivityResultIapStoreHelperHandler}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnActivityResultIapStoreHelperHandler" , "" ) } ,
{ "//$${gameActivityPreConfigRulesParseAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityPreConfigRulesParseAdditions" , "" ) } ,
{ "//$${gameActivityPostConfigRulesAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityPostConfigRulesAdditions" , "" ) } ,
{ "//$${gameActivityFinalizeConfigRulesAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityFinalizeConfigRulesAdditions" , "" ) } ,
{ "//$${gameActivityBeforeConfigRulesAppliedAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityBeforeConfigRulesAppliedAdditions" , "" ) } ,
{ "//$${gameActivityAfterMainViewCreatedAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityAfterMainViewCreatedAdditions" , "" ) } ,
{ "//$${gameActivityResizeKeyboardAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityResizeKeyboardAdditions" , "" ) } ,
{ "//$${gameActivityLoggerCallbackAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityLoggerCallbackAdditions" , "" ) } ,
{ "//$${gameActivityGetCommandLineAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityGetCommandLineAdditions" , "" ) } ,
{ "//$${gameActivityGetLoginIdAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityGetLoginIdAdditions" , "" ) } ,
{ "//$${gameActivityGetFunnelIdAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityGetFunnelIdAdditions" , "" ) } ,
{ "//$${gameActivityAllowedRemoteNotificationsAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityAllowedRemoteNotificationsAdditions" , "" ) } ,
{ "//$${gameActivityAndroidThunkJavaIapBeginPurchase}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityAndroidThunkJavaIapBeginPurchase" , "" ) } ,
{ "//$${gameActivityIapSetupServiceAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityIapSetupServiceAdditions" , "" ) } ,
{ "//$${gameActivityOnRestartApplicationAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityOnRestartApplicationAdditions" , "" ) } ,
{ "//$${gameActivityForceQuitAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivityForceQuitAdditions" , "" ) } ,
{ "//$${gameActivitySetCrashContextData}$$" , UPL . ProcessPluginNode ( NDKArch , "gameActivitySetCrashContextData" , "" ) } ,
{ "//$${soLoadLibrary}$$" , UPL . ProcessPluginNode ( NDKArch , "soLoadLibrary" , LoadLibraryDefaults ) } ,
{ "$${gameActivitySuperClass}$$" , SuperClassDefault } ,
} ;
string [ ] TemplateSrc = File . ReadAllLines ( SourceFilename ) ;
string [ ] ? TemplateDest = File . Exists ( DestFilename ) ? File . ReadAllLines ( DestFilename ) : null ;
bool TemplateChanged = false ;
for ( int LineIndex = 0 ; LineIndex < TemplateSrc . Length ; + + LineIndex )
{
string SrcLine = TemplateSrc [ LineIndex ] ;
bool Changed = false ;
foreach ( KeyValuePair < string , string > KVP in Replacements )
{
if ( SrcLine . Contains ( KVP . Key ) )
{
SrcLine = SrcLine . Replace ( KVP . Key , KVP . Value ) ;
Changed = true ;
}
}
if ( Changed )
{
TemplateSrc [ LineIndex ] = SrcLine ;
TemplateChanged = true ;
}
}
if ( TemplateChanged )
{
// deal with insertions of newlines
TemplateSrc = string . Join ( "\n" , TemplateSrc ) . Split ( new [ ] { "\r\n" , "\r" , "\n" } , StringSplitOptions . None ) ;
}
if ( TemplateDest = = null | | TemplateSrc . Length ! = TemplateDest . Length | | ! TemplateSrc . SequenceEqual ( TemplateDest ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "" ) ;
Logger . LogInformation ( "==== Writing new GameActivity.java file to {DestFile} ====" , DestFilename ) ;
2022-05-24 19:41:41 -04:00
File . WriteAllLines ( DestFilename , TemplateSrc ) ;
}
}
private void UpdateGameApplication ( string UnrealArch , string NDKArch , string EngineDir , string UnrealBuildPath )
{
string SourceFilename = Path . Combine ( EngineDir , "Build" , "Android" , "Java" , "src" , "com" , "epicgames" , "unreal" , "GameApplication.java.template" ) ;
string DestFilename = Path . Combine ( UnrealBuildPath , "src" , "com" , "epicgames" , "unreal" , "GameApplication.java" ) ;
Dictionary < string , string > Replacements = new Dictionary < string , string > {
{ "//$${gameApplicationImportAdditions}$$" , UPL ! . ProcessPluginNode ( NDKArch , "gameApplicationImportAdditions" , "" ) } ,
{ "//$${gameApplicationOnCreateAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameApplicationOnCreateAdditions" , "" ) } ,
{ "//$${gameApplicationAttachBaseContextAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameApplicationAttachBaseContextAdditions" , "" ) } ,
{ "//$${gameApplicationOnLowMemoryAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameApplicationOnLowMemoryAdditions" , "" ) } ,
{ "//$${gameApplicationOnTrimMemoryAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameApplicationOnTrimMemoryAdditions" , "" ) } ,
{ "//$${gameApplicationOnConfigurationChangedAdditions}$$" , UPL . ProcessPluginNode ( NDKArch , "gameApplicationOnConfigurationChangedAdditions" , "" ) } ,
} ;
string [ ] TemplateSrc = File . ReadAllLines ( SourceFilename ) ;
string [ ] ? TemplateDest = File . Exists ( DestFilename ) ? File . ReadAllLines ( DestFilename ) : null ;
bool TemplateChanged = false ;
for ( int LineIndex = 0 ; LineIndex < TemplateSrc . Length ; + + LineIndex )
{
string SrcLine = TemplateSrc [ LineIndex ] ;
bool Changed = false ;
foreach ( KeyValuePair < string , string > KVP in Replacements )
{
if ( SrcLine . Contains ( KVP . Key ) )
{
SrcLine = SrcLine . Replace ( KVP . Key , KVP . Value ) ;
Changed = true ;
}
}
if ( Changed )
{
TemplateSrc [ LineIndex ] = SrcLine ;
TemplateChanged = true ;
}
}
if ( TemplateChanged )
{
// deal with insertions of newlines
TemplateSrc = string . Join ( "\n" , TemplateSrc ) . Split ( new [ ] { "\r\n" , "\r" , "\n" } , StringSplitOptions . None ) ;
}
if ( TemplateDest = = null | | TemplateSrc . Length ! = TemplateDest . Length | | ! TemplateSrc . SequenceEqual ( TemplateDest ) )
{
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "" ) ;
Logger . LogInformation ( "==== Writing new GameApplication.java file to {DestFile} ====" , DestFilename ) ;
2022-05-24 19:41:41 -04:00
File . WriteAllLines ( DestFilename , TemplateSrc ) ;
}
}
private void CreateAdditonalBuildPathFiles ( string NDKArch , string UnrealBuildPath , XDocument FilesToAdd )
{
Dictionary < string , string? > PathsAndRootEls = new Dictionary < string , string? > ( ) ;
foreach ( XElement Element in FilesToAdd . Root ! . Elements ( ) )
{
string RelPath = Element . Value ;
if ( RelPath ! = null )
{
XAttribute ? TypeAttr = Element . Attribute ( "rootEl" ) ;
PathsAndRootEls [ RelPath ] = TypeAttr ? . Value ;
}
}
foreach ( KeyValuePair < string , string? > Entry in PathsAndRootEls )
{
string UPLNodeName = Entry . Key . Replace ( "/" , "__" ) . Replace ( "." , "__" ) ;
string Content ;
if ( Entry . Value = = null )
{
// no root element, assume not XML
Content = UPL ! . ProcessPluginNode ( NDKArch , UPLNodeName , "" ) ;
}
else
{
XDocument ContentDoc = new XDocument ( new XElement ( Entry . Value ) ) ;
UPL ! . ProcessPluginNode ( NDKArch , UPLNodeName , "" , ref ContentDoc ) ;
Content = XML_HEADER + "\n" + ContentDoc . ToString ( ) ;
}
string DestPath = Path . Combine ( UnrealBuildPath , Entry . Key ) ;
if ( ! File . Exists ( DestPath ) | | File . ReadAllText ( DestPath ) ! = Content )
{
File . WriteAllText ( DestPath , Content ) ;
}
}
}
private AndroidAARHandler CreateAARHandler ( string EngineDir , string UnrealBuildPath , List < string > NDKArches , bool HandleDependencies = true )
{
AndroidAARHandler AARHandler = new AndroidAARHandler ( ) ;
string ImportList = "" ;
// Get some common paths
string AndroidHome = Environment . ExpandEnvironmentVariables ( "%ANDROID_HOME%" ) . TrimEnd ( '/' , '\\' ) ;
EngineDir = EngineDir . TrimEnd ( '/' , '\\' ) ;
// Add the AARs from the default aar-imports.txt
// format: Package,Name,Version
string ImportsFile = Path . Combine ( UnrealBuildPath , "aar-imports.txt" ) ;
if ( File . Exists ( ImportsFile ) )
{
ImportList = File . ReadAllText ( ImportsFile ) ;
}
// Run the UPL imports section for each architecture and add any new imports (duplicates will be removed)
foreach ( string NDKArch in NDKArches )
{
ImportList = UPL ! . ProcessPluginNode ( NDKArch , "AARImports" , ImportList ) ;
}
// Add the final list of imports and get dependencies
foreach ( string Line in ImportList . Split ( '\n' ) )
{
string Trimmed = Line . Trim ( ' ' , '\r' ) ;
if ( Trimmed . StartsWith ( "repository " ) )
{
string DirectoryPath = Trimmed . Substring ( 11 ) . Trim ( ' ' ) . TrimEnd ( '/' , '\\' ) ;
DirectoryPath = DirectoryPath . Replace ( "$(ENGINEDIR)" , EngineDir ) ;
DirectoryPath = DirectoryPath . Replace ( "$(ANDROID_HOME)" , AndroidHome ) ;
DirectoryPath = DirectoryPath . Replace ( '\\' , Path . DirectorySeparatorChar ) . Replace ( '/' , Path . DirectorySeparatorChar ) ;
2022-05-25 19:55:37 -04:00
AARHandler . AddRepository ( DirectoryPath , Logger ) ;
2022-05-24 19:41:41 -04:00
}
else if ( Trimmed . StartsWith ( "repositories " ) )
{
string DirectoryPath = Trimmed . Substring ( 13 ) . Trim ( ' ' ) . TrimEnd ( '/' , '\\' ) ;
DirectoryPath = DirectoryPath . Replace ( "$(ENGINEDIR)" , EngineDir ) ;
DirectoryPath = DirectoryPath . Replace ( "$(ANDROID_HOME)" , AndroidHome ) ;
DirectoryPath = DirectoryPath . Replace ( '\\' , Path . DirectorySeparatorChar ) . Replace ( '/' , Path . DirectorySeparatorChar ) ;
2022-05-25 19:55:37 -04:00
AARHandler . AddRepositories ( DirectoryPath , "m2repository" , Logger ) ;
2022-05-24 19:41:41 -04:00
}
else
{
string [ ] Sections = Trimmed . Split ( ',' ) ;
if ( Sections . Length = = 3 )
{
string PackageName = Sections [ 0 ] . Trim ( ' ' ) ;
string BaseName = Sections [ 1 ] . Trim ( ' ' ) ;
string Version = Sections [ 2 ] . Trim ( ' ' ) ;
2022-05-25 19:55:37 -04:00
Logger . LogInformation ( "AARImports: {PackageName}, {BaseName}, {Version}" , PackageName , BaseName , Version ) ;
AARHandler . AddNewAAR ( PackageName , BaseName , Version , Logger , HandleDependencies ) ;
2022-05-24 19:41:41 -04:00
}
}
}
return AARHandler ;
}
private void PrepareJavaLibsForGradle ( string JavaLibsDir , string UnrealBuildGradlePath , string InMinSdkVersion , string InTargetSdkVersion , string CompileSDKVersion , string BuildToolsVersion , string NDKArch )
{
StringBuilder SettingsGradleContent = new StringBuilder ( ) ;
StringBuilder ProjectDependencyContent = new StringBuilder ( ) ;
SettingsGradleContent . AppendLine ( "rootProject.name='app'" ) ;
SettingsGradleContent . AppendLine ( "include ':app'" ) ;
ProjectDependencyContent . AppendLine ( "dependencies {" ) ;
string [ ] LibDirs = Directory . GetDirectories ( JavaLibsDir ) ;
foreach ( string LibDir in LibDirs )
{
string RelativePath = Path . GetFileName ( LibDir ) ;
SettingsGradleContent . AppendLine ( string . Format ( "include ':{0}'" , RelativePath ) ) ;
ProjectDependencyContent . AppendLine ( string . Format ( "\timplementation project(':{0}')" , RelativePath ) ) ;
string GradleProjectPath = Path . Combine ( UnrealBuildGradlePath , RelativePath ) ;
string GradleProjectMainPath = Path . Combine ( GradleProjectPath , "src" , "main" ) ;
string ManifestFilename = Path . Combine ( LibDir , "AndroidManifest.xml" ) ;
string GradleManifest = Path . Combine ( GradleProjectMainPath , "AndroidManifest.xml" ) ;
MakeDirectoryIfRequired ( GradleManifest ) ;
// Copy parts were they need to be
CleanCopyDirectory ( Path . Combine ( LibDir , "assets" ) , Path . Combine ( GradleProjectPath , "assets" ) ) ;
CleanCopyDirectory ( Path . Combine ( LibDir , "libs" ) , Path . Combine ( GradleProjectPath , "libs" ) ) ;
CleanCopyDirectory ( Path . Combine ( LibDir , "res" ) , Path . Combine ( GradleProjectMainPath , "res" ) ) ;
// If our lib already has a src/main/java folder, don't put things into a java folder
string SrcDirectory = Path . Combine ( LibDir , "src" , "main" ) ;
if ( Directory . Exists ( Path . Combine ( SrcDirectory , "java" ) ) )
{
CleanCopyDirectory ( SrcDirectory , GradleProjectMainPath ) ;
}
else
{
CleanCopyDirectory ( Path . Combine ( LibDir , "src" ) , Path . Combine ( GradleProjectMainPath , "java" ) ) ;
}
// Now generate a build.gradle from the manifest
StringBuilder BuildGradleContent = new StringBuilder ( ) ;
BuildGradleContent . AppendLine ( "apply plugin: 'com.android.library'" ) ;
BuildGradleContent . AppendLine ( "android {" ) ;
BuildGradleContent . AppendLine ( string . Format ( "\tcompileSdkVersion {0}" , CompileSDKVersion ) ) ;
BuildGradleContent . AppendLine ( "\tdefaultConfig {" ) ;
// Try to get the SDK target from the AndroidManifest.xml
string VersionCode = "" ;
string VersionName = "" ;
string MinSdkVersion = InMinSdkVersion ;
string TargetSdkVersion = InTargetSdkVersion ;
XDocument ManifestXML ;
if ( File . Exists ( ManifestFilename ) )
{
try
{
ManifestXML = XDocument . Load ( ManifestFilename ) ;
XAttribute ? VersionCodeAttr = ManifestXML . Root ! . Attribute ( XName . Get ( "versionCode" , "http://schemas.android.com/apk/res/android" ) ) ;
if ( VersionCodeAttr ! = null )
{
VersionCode = VersionCodeAttr . Value ;
}
XAttribute ? VersionNameAttr = ManifestXML . Root . Attribute ( XName . Get ( "versionName" , "http://schemas.android.com/apk/res/android" ) ) ;
if ( VersionNameAttr ! = null )
{
VersionName = VersionNameAttr . Value ;
}
XElement ? UseSDKNode = null ;
foreach ( XElement WorkNode in ManifestXML . Elements ( ) . First ( ) . Descendants ( "uses-sdk" ) )
{
UseSDKNode = WorkNode ;
XAttribute ? MinSdkVersionAttr = WorkNode . Attribute ( XName . Get ( "minSdkVersion" , "http://schemas.android.com/apk/res/android" ) ) ;
if ( MinSdkVersionAttr ! = null )
{
MinSdkVersion = MinSdkVersionAttr . Value ;
}
XAttribute ? TargetSdkVersionAttr = WorkNode . Attribute ( XName . Get ( "targetSdkVersion" , "http://schemas.android.com/apk/res/android" ) ) ;
if ( TargetSdkVersionAttr ! = null )
{
TargetSdkVersion = TargetSdkVersionAttr . Value ;
}
}
if ( UseSDKNode ! = null )
{
UseSDKNode . Remove ( ) ;
}
// rewrite the manifest if different
String NewManifestText = ManifestXML . ToString ( ) ;
String OldManifestText = "" ;
if ( File . Exists ( GradleManifest ) )
{
OldManifestText = File . ReadAllText ( GradleManifest ) ;
}
if ( NewManifestText ! = OldManifestText )
{
File . WriteAllText ( GradleManifest , NewManifestText ) ;
}
}
catch ( Exception e )
{
2022-05-25 19:55:37 -04:00
Logger . LogError ( e , "AAR Manifest file {FileName} parsing error! {Ex}" , ManifestFilename , e ) ;
2022-05-24 19:41:41 -04:00
}
}
if ( VersionCode ! = "" )
{
BuildGradleContent . AppendLine ( string . Format ( "\t\tversionCode {0}" , VersionCode ) ) ;
}
if ( VersionName ! = "" )
{
BuildGradleContent . AppendLine ( string . Format ( "\t\tversionName \"{0}\"" , VersionName ) ) ;
}
if ( MinSdkVersion ! = "" )
{
BuildGradleContent . AppendLine ( string . Format ( "\t\tminSdkVersion = {0}" , MinSdkVersion ) ) ;
}
if ( TargetSdkVersion ! = "" )
{
BuildGradleContent . AppendLine ( string . Format ( "\t\ttargetSdkVersion = {0}" , TargetSdkVersion ) ) ;
}
BuildGradleContent . AppendLine ( "\t}" ) ;
BuildGradleContent . AppendLine ( "}" ) ;
string AdditionsGradleFilename = Path . Combine ( LibDir , "additions.gradle" ) ;
if ( File . Exists ( AdditionsGradleFilename ) )
{
string [ ] AdditionsLines = File . ReadAllLines ( AdditionsGradleFilename ) ;
foreach ( string LineContents in AdditionsLines )
{
BuildGradleContent . AppendLine ( LineContents ) ;
}
}
// rewrite the build.gradle if different
string BuildGradleFilename = Path . Combine ( GradleProjectPath , "build.gradle" ) ;
String NewBuildGradleText = BuildGradleContent . ToString ( ) ;
String OldBuildGradleText = "" ;
if ( File . Exists ( BuildGradleFilename ) )
{
OldBuildGradleText = File . ReadAllText ( BuildGradleFilename ) ;
}
if ( NewBuildGradleText ! = OldBuildGradleText )
{
File . WriteAllText ( BuildGradleFilename , NewBuildGradleText ) ;
}
}
ProjectDependencyContent . AppendLine ( "}" ) ;
// Add any UPL settingsGradleAdditions
SettingsGradleContent . Append ( UPL ! . ProcessPluginNode ( NDKArch , "settingsGradleAdditions" , "" ) ) ;
string SettingsGradleFilename = Path . Combine ( UnrealBuildGradlePath , "settings.gradle" ) ;
File . WriteAllText ( SettingsGradleFilename , SettingsGradleContent . ToString ( ) ) ;
string ProjectsGradleFilename = Path . Combine ( UnrealBuildGradlePath , "app" , "projects.gradle" ) ;
File . WriteAllText ( ProjectsGradleFilename , ProjectDependencyContent . ToString ( ) ) ;
}
private void GenerateGradleAARImports ( string EngineDir , string UnrealBuildPath , List < string > NDKArches )
{
AndroidAARHandler AARHandler = CreateAARHandler ( EngineDir , UnrealBuildPath , NDKArches , false ) ;
StringBuilder AARImportsContent = new StringBuilder ( ) ;
// Add repositories
AARImportsContent . AppendLine ( "repositories {" ) ;
foreach ( string Repository in AARHandler . Repositories ! )
{
string RepositoryPath = Path . GetFullPath ( Repository ) . Replace ( '\\' , '/' ) ;
AARImportsContent . AppendLine ( "\tmaven { url uri('" + RepositoryPath + "') }" ) ;
}
AARImportsContent . AppendLine ( "}" ) ;
// Add dependencies
AARImportsContent . AppendLine ( "dependencies {" ) ;
foreach ( AndroidAARHandler . AndroidAAREntry Dependency in AARHandler . AARList ! )
{
AARImportsContent . AppendLine ( string . Format ( "\timplementation '{0}:{1}:{2}'" , Dependency . Filename , Dependency . BaseName , Dependency . Version ) ) ;
}
AARImportsContent . AppendLine ( "}" ) ;
string AARImportsFilename = Path . Combine ( UnrealBuildPath , "gradle" , "app" , "aar-imports.gradle" ) ;
File . WriteAllText ( AARImportsFilename , AARImportsContent . ToString ( ) ) ;
}
}
}