Files
UnrealEngineUWP/Engine/Source/Programs/UnrealBuildTool/Android/UEDeployAndroid.cs
James Golding 63fa5e47d7 Remove unused AndroidCommandPath string to remove clang warning
#codereview josh.adams

[CL 2352883 by James Golding in Main branch]
2014-11-07 09:51:21 -05:00

877 lines
32 KiB
C#

// Copyright 1998-2014 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;
namespace UnrealBuildTool.Android
{
public class UEDeployAndroid : UEBuildDeploy
{
/**
* Register the platform with the UEBuildDeploy class
*/
public override void RegisterBuildDeploy()
{
string NDKPath = Environment.GetEnvironmentVariable("ANDROID_HOME");
// we don't have an NDKROOT specified
if (String.IsNullOrEmpty(NDKPath))
{
Log.TraceVerbose(" Unable to register Android deployment class because the ANDROID_HOME environment variable isn't set or points to something that doesn't exist");
}
else
{
UEBuildDeploy.RegisterBuildDeploy(UnrealTargetPlatform.Android, this);
}
}
/** Internal usage for GetApiLevel */
private static List<string> PossibleApiLevels = null;
/** Simple function to pipe output asynchronously */
private static 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]);
}
}
}
}
static private string CachedSDKLevel = null;
private static string GetSdkApiLevel()
{
if (CachedSDKLevel == null)
{
// default to looking on disk for latest API level
string Target = AndroidPlatform.AndroidSdkApiTarget;
// if we want to use whatever version the ndk uses, then use that
if (Target == "matchndk")
{
Target = AndroidToolChain.GetNdkApiLevel();
}
// run a command and capture output
if (Target == "latest")
{
// we expect there to be one, so use the first one
string AndroidCommandPath = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/tools/android.bat");
var ExeInfo = new ProcessStartInfo(AndroidCommandPath, "list targets");
ExeInfo.UseShellExecute = false;
ExeInfo.RedirectStandardOutput = true;
using (var GameProcess = Process.Start(ExeInfo))
{
PossibleApiLevels = new List<string>();
GameProcess.BeginOutputReadLine();
GameProcess.OutputDataReceived += ParseApiLevel;
GameProcess.WaitForExit();
}
if (PossibleApiLevels != null && PossibleApiLevels.Count > 0)
{
Target = AndroidToolChain.GetLargestApiLevel(PossibleApiLevels.ToArray());
}
else
{
throw new BuildException("Can't make an APK an API installed (see \"android.bat list targets\")");
}
}
Console.WriteLine("Building Java with SDK API '{0}'", Target);
CachedSDKLevel = Target;
}
return CachedSDKLevel;
}
private static string GetAntPath()
{
// look up an ANT_HOME env var
string AntHome = Environment.GetEnvironmentVariable("ANT_HOME");
if (!string.IsNullOrEmpty(AntHome) && Directory.Exists(AntHome))
{
string AntPath = AntHome + "/bin/ant.bat";
// use it if found
if (File.Exists(AntPath))
{
return AntPath;
}
}
// otherwise, look in the eclipse install for the ant plugin (matches the unzipped Android ADT from Google)
string PluginDir = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/../eclipse/plugins");
if (Directory.Exists(PluginDir))
{
string[] Plugins = Directory.GetDirectories(PluginDir, "org.apache.ant*");
// use the first one with ant.bat
if (Plugins.Length > 0)
{
foreach (string PluginName in Plugins)
{
string AntPath = PluginName + "/bin/ant.bat";
// use it if found
if (File.Exists(AntPath))
{
return AntPath;
}
}
}
}
throw new BuildException("Unable to find ant.bat (via %ANT_HOME% or %ANDROID_HOME%/../eclipse/plugins/org.apache.ant*");
}
private static void CopyFileDirectory(string SourceDir, string DestDir, Dictionary<string,string> Replacements)
{
if (!Directory.Exists(SourceDir))
{
return;
}
string[] Files = Directory.GetFiles(SourceDir, "*.*", SearchOption.AllDirectories);
foreach (string Filename in Files)
{
// 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));
if (File.Exists(DestFilename))
{
File.Delete(DestFilename);
}
// 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")
{
string Contents = File.ReadAllText(Filename);
// replace some varaibles
foreach (var Pair in Replacements)
{
Contents = Contents.Replace(Pair.Key, Pair.Value);
}
// write out file
File.WriteAllText(DestFilename, Contents);
}
else
{
File.Copy(Filename, DestFilename);
// remove any read only flags
FileInfo DestFileInfo = new FileInfo(DestFilename);
DestFileInfo.Attributes = DestFileInfo.Attributes & ~FileAttributes.ReadOnly;
}
}
}
private static void DeleteDirectory(string InPath, string SubDirectoryToKeep="")
{
// 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)
{
Log.TraceInformation("Failed to delete all files in directory {0}. Continuing on...", InPath);
}
}
string[] Dirs = Directory.GetDirectories(InPath, "*.*", SearchOption.TopDirectoryOnly);
foreach (string Dir in Dirs)
{
DeleteDirectory(Dir, SubDirectoryToKeep);
// try to delete the directory, but allow it to fail (due to SubDirectoryToKeep still existing)
try
{
Directory.Delete(Dir);
}
catch (Exception)
{
// do nothing
}
}
}
public string GetUE4BuildFilePath(String EngineDirectory)
{
return Path.GetFullPath(Path.Combine(EngineDirectory, "Build/Android/Java"));
}
public string GetUE4JavaFilePath(String EngineDirectory)
{
return Path.GetFullPath(Path.Combine(GetUE4BuildFilePath(EngineDirectory), "src/com/epicgames/ue4"));
}
public string GetUE4JavaBuildSettingsFileName(String EngineDirectory)
{
return Path.Combine(GetUE4JavaFilePath(EngineDirectory), "JavaBuildSettings.java");
}
public void WriteJavaBuildSettingsFile(string FileName, bool OBBinAPK)
{
// (!UEBuildConfiguration.bOBBinAPK ? "PackageType.AMAZON" : /*bPackageForGoogle ? "PackageType.GOOGLE" :*/ "PackageType.DEVELOPMENT") + ";\n");
string Setting = OBBinAPK ? "AMAZON" : "DEVELOPMENT";
if (!File.Exists(FileName) || ShouldWriteJavaBuildSettingsFile(FileName, Setting))
{
Log.TraceInformation("\n===={0}====WRITING JAVABUILDSETTINGS.JAVA========================================================", DateTime.Now.ToString());
StringBuilder BuildSettings = new StringBuilder("package com.epicgames.ue4;\npublic class JavaBuildSettings\n{\n");
BuildSettings.Append("\tpublic enum PackageType {AMAZON, GOOGLE, DEVELOPMENT};\n");
BuildSettings.Append("\tpublic static final PackageType PACKAGING = PackageType." + Setting + ";\n");
BuildSettings.Append("}\n");
File.WriteAllText(FileName, BuildSettings.ToString());
}
}
public bool ShouldWriteJavaBuildSettingsFile(string FileName, string setting)
{
var fileContent = File.ReadAllLines(FileName);
if (fileContent.Length < 5)
return true;
var packageLine = fileContent[4]; // We know this to be true... because we write it below...
int location = packageLine.IndexOf("PACKAGING") + 12 + 12; // + ("PACKAGING = ") + ("PackageType.")
if (location == -1)
return true;
return String.Compare(setting, packageLine.Substring(location, Math.Min(packageLine.Length - location, setting.Length))) != 0;
}
private static string GetNDKArch(string UE4Arch)
{
switch (UE4Arch)
{
case "-armv7": return "armeabi-v7a";
case "-x86": return "x86";
default: throw new BuildException("Unknown UE4 architecture {0}", UE4Arch);
}
}
public static string GetUE4Arch(string NDKArch)
{
switch (NDKArch)
{
case "armeabi-v7a": return "-armv7";
case "x86": return "-x86";
default: throw new BuildException("Unknown NDK architecture '{0}'", NDKArch);
}
}
private static void CopySTL(string UE4BuildPath, string UE4Arch)
{
string Arch = GetNDKArch(UE4Arch);
string GccVersion = "4.8";
if (!Directory.Exists(Environment.ExpandEnvironmentVariables("%NDKROOT%/sources/cxx-stl/gnu-libstdc++/4.8")))
{
GccVersion = "4.6";
}
// copy it in!
string SourceSTLSOName = Environment.ExpandEnvironmentVariables("%NDKROOT%/sources/cxx-stl/gnu-libstdc++/") + GccVersion + "/libs/" + Arch + "/libgnustl_shared.so";
string FinalSTLSOName = UE4BuildPath + "/libs/" + Arch + "/libgnustl_shared.so";
Directory.CreateDirectory(Path.GetDirectoryName(FinalSTLSOName));
File.Copy(SourceSTLSOName, FinalSTLSOName);
}
//@TODO: To enable the NVIDIA Gfx Debugger
private static void CopyGfxDebugger(string UE4BuildPath, string UE4Arch)
{
// string Arch = GetNDKArch(UE4Arch);
// Directory.CreateDirectory(UE4BuildPath + "/libs/" + Arch);
// File.Copy("E:/Dev2/UE4-Clean/Engine/Source/ThirdParty/NVIDIA/TegraGfxDebugger/libNvPmApi.Core.so", UE4BuildPath + "/libs/" + Arch + "/libNvPmApi.Core.so");
// File.Copy("E:/Dev2/UE4-Clean/Engine/Source/ThirdParty/NVIDIA/TegraGfxDebugger/libTegra_gfx_debugger.so", UE4BuildPath + "/libs/" + Arch + "/libTegra_gfx_debugger.so");
}
private static void RunCommandLineProgramAndThrowOnError(string WorkingDirectory, string Command, string Params, string OverrideDesc=null, bool bUseShellExecute=false)
{
if (OverrideDesc == null)
{
Log.TraceInformation("\nRunning: " + Command + " " + Params);
}
else if (OverrideDesc != "")
{
Log.TraceInformation(OverrideDesc);
Log.TraceVerbose("\nRunning: " + Command + " " + Params);
}
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 static bool IsAPIVersionCurrent(string PropertiesFileLocation)
{
string ProjectProperties = Path.Combine(PropertiesFileLocation, "project.properties");
if (File.Exists(ProjectProperties))
{
string[] Contents = File.ReadAllLines(ProjectProperties);
foreach (string Line in Contents)
{
// look for the special line
if (Line.StartsWith("target="))
{
// skip over target=
string Version = Line.Substring(7);
// do they match?
return string.Compare(Version, GetSdkApiLevel(), true) == 0;
}
}
}
// if any other case happens, we are not current
return false;
}
private void UpdateProjectProperties(string UE4BuildPath, string ProjectName)
{
Log.TraceInformation("\n===={0}====UPDATING BUILD CONFIGURATION FILES====================================================", DateTime.Now.ToString());
// get all of the libs (from engine + project)
string JavaLibsDir = Path.Combine(UE4BuildPath, "JavaLibs");
string[] LibDirs = Directory.GetDirectories(JavaLibsDir);
// get existing project.properties lines (if any)
string ProjectPropertiesFile = Path.Combine(UE4BuildPath, "project.properties");
string[] PropertiesLines = new string[] { };
if (File.Exists(ProjectPropertiesFile))
{
PropertiesLines = File.ReadAllLines(ProjectPropertiesFile);
}
// figure out how many libraries were already listed (if there were more than this listed, then we need to start the file over, because we need to unreference a library)
int NumOutstandingAlreadyReferencedLibs = 0;
foreach (string Line in PropertiesLines)
{
if (Line.StartsWith("android.library.reference."))
{
NumOutstandingAlreadyReferencedLibs++;
}
}
// now go through each one and verify they are listed in project properties, and if not, add them
List<string> LibsToBeAdded = new List<string>();
foreach (string LibDir in LibDirs)
{
// put it in terms of the subdirectory that would be in the project.properties
string RelativePath = "JavaLibs/" + Path.GetFileName(LibDir);
// now look for this in the existing file
bool bWasReferencedAlready = false;
foreach (string Line in PropertiesLines)
{
if (Line.StartsWith("android.library.reference.") && Line.EndsWith(RelativePath))
{
// this lib was already referenced, don't need to readd
bWasReferencedAlready = true;
break;
}
}
if (bWasReferencedAlready)
{
// if it was, no further action needed, and count it off
NumOutstandingAlreadyReferencedLibs--;
}
else
{
// otherwise, we need to add it to the project properties
LibsToBeAdded.Add(RelativePath);
}
}
// now at this point, if there are any outstanding already referenced libs, we have too many, so we have to start over
if (NumOutstandingAlreadyReferencedLibs > 0)
{
// @todo android: If a user had a project.properties in the game, NEVER do this
Log.TraceInformation("There were too many libs already referenced in project.properties, tossing it");
File.Delete(ProjectPropertiesFile);
LibsToBeAdded.Clear();
foreach (string LibDir in LibDirs)
{
// put it in terms of the subdirectory that would be in the project.properties
LibsToBeAdded.Add("JavaLibs/" + Path.GetFileName(LibDir));
}
}
// now update the project for each library
string AndroidCommandPath = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/tools/android.bat");
string UpdateCommandLine = "--silent update project --subprojects --name " + ProjectName + " --path . --target " + GetSdkApiLevel();
foreach (string Lib in LibsToBeAdded)
{
UpdateCommandLine += " --library " + Lib;
// make sure each library has a build.xml - --subprojects doesn't create build.xml files, but it will create project.properties
// and later code needs each lib to have a build.xml
if (!File.Exists(Path.Combine(Lib, "build.xml")))
{
RunCommandLineProgramAndThrowOnError(UE4BuildPath, AndroidCommandPath, "--silent update lib-project --path " + Lib + " --target " + GetSdkApiLevel(), "");
}
}
RunCommandLineProgramAndThrowOnError(UE4BuildPath, AndroidCommandPath, UpdateCommandLine, "Updating project.properties, local.properties, and build.xml...");
}
private void MakeApk(string ProjectName, string ProjectDirectory, string OutputPath, string EngineDirectory, bool bForDistribution, string CookFlavor, bool bMakeSeparateApks, bool bIncrementalPackage)
{
Log.TraceInformation("\n===={0}====PREPARING TO MAKE APK=================================================================", DateTime.Now.ToString());
// cache some tools paths
string NDKBuildPath = Environment.ExpandEnvironmentVariables("%NDKROOT%/ndk-build.cmd");
// set up some directory info
string IntermediateAndroidPath = Path.Combine(ProjectDirectory, "Intermediate/Android/");
string UE4BuildPath = Path.Combine(IntermediateAndroidPath, "APK");
string UE4BuildFilesPath = GetUE4BuildFilePath(EngineDirectory);
string GameBuildFilesPath = Path.Combine(ProjectDirectory, "Build/Android");
string[] Arches = AndroidToolChain.GetAllArchitectures();
string[] GPUArchitectures = AndroidToolChain.GetAllGPUArchitectures();
int NumArches = Arches.Length * GPUArchitectures.Length;
// See if we need to create a 'default' Java Build settings file if one doesn't exist (if it does exist we have to assume it has been setup correctly)
string UE4JavaBuildSettingsFileName = GetUE4JavaBuildSettingsFileName(EngineDirectory);
WriteJavaBuildSettingsFile(UE4JavaBuildSettingsFileName, UEBuildConfiguration.bOBBinAPK);
// if the last packaged version is wrong, then we can skip the checking and do it
if (!IsAPIVersionCurrent(UE4BuildPath))
{
Log.TraceInformation("Output .apk file(s) were made with a different API version, forcing repackage.");
}
else
{
// check if so's are up to date against various inputs
bool bAllInputsCurrent = true;
foreach (string Arch in Arches)
{
foreach (string GPUArch in GPUArchitectures)
{
string SourceSOName = AndroidToolChain.InlineArchName(OutputPath, Arch, GPUArch);
// if the source binary was UE4Game, replace it with the new project name, when re-packaging a binary only build
string ApkFilename = Path.GetFileNameWithoutExtension(OutputPath).Replace("UE4Game", 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, GPUArch);
}
// 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(UE4BuildFilesPath, "*.*", SearchOption.AllDirectories));
if (Directory.Exists(GameBuildFilesPath))
{
InputFiles.AddRange(Directory.EnumerateFiles(GameBuildFilesPath, "*.*", SearchOption.AllDirectories));
}
// rebuild if .ini files change
// @todo android: programmatically determine if any .ini setting changed?
InputFiles.Add(Path.Combine(EngineDirectory, "Config\\BaseEngine.ini"));
InputFiles.Add(Path.Combine(ProjectDirectory, "Config\\DefaultEngine.ini"));
// make sure changed java settings will rebuild apk
InputFiles.Add(UE4JavaBuildSettingsFileName);
// rebuild if .pak files exist for OBB in APK case
if (UEBuildConfiguration.bOBBinAPK)
{
string PAKFileLocation = ProjectDirectory + "/Saved/StagedBuilds/Android" + CookFlavor + "/" + ProjectName + "/Content/Paks";
if (Directory.Exists(PAKFileLocation))
{
var PakFiles = Directory.EnumerateFiles(PAKFileLocation, "*.pak", SearchOption.TopDirectoryOnly);
foreach (var Name in PakFiles)
{
InputFiles.Add(Name);
}
}
}
// look for any newer input file
DateTime ApkTime = File.GetLastWriteTimeUtc(DestApkName);
foreach (var 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;
Log.TraceInformation("{0} is out of date due to newer input file {1}", DestApkName, InputFileName);
break;
}
}
}
}
}
if (bAllInputsCurrent)
{
Log.TraceInformation("Output .apk file(s) are up to date (compared to the .so and .java input files)");
return;
}
}
// Once for all arches code:
// make up a dictionary of strings to replace in the Manifest file
Dictionary<string, string> Replacements = new Dictionary<string, string>();
Replacements.Add("${EXECUTABLE_NAME}", ProjectName);
// distribution apps can't be debuggable, so if it was set to true, set it to false:
if (bForDistribution)
{
Replacements.Add("android:debuggable=\"true\"", "android:debuggable=\"false\"");
}
if (!bIncrementalPackage)
{
// Wipe the Intermediate/Build/APK directory first, except for dexedLibs, because Google Services takes FOREVER to predex, and it almost never changes
// so allow the ANT checking to win here - if this grows a bit with extra libs, it's fine, it _should_ only pull in dexedLibs it needs
Log.TraceInformation("Performing complete package - wiping {0}, except for predexedLibs", UE4BuildPath);
DeleteDirectory(UE4BuildPath, "dexedLibs");
}
// If we are packaging for Amazon then we need to copy the PAK files to the correct location
// Currently we'll just support 1 of 'em
if (UEBuildConfiguration.bOBBinAPK)
{
string PAKFileLocation = ProjectDirectory + "/Saved/StagedBuilds/Android" + CookFlavor + "/" + ProjectName + "/Content/Paks";
Console.WriteLine("Pak location {0}", PAKFileLocation);
string PAKFileDestination = UE4BuildPath + "/assets";
Console.WriteLine("Pak destination location {0}", PAKFileDestination);
if (Directory.Exists(PAKFileLocation))
{
Directory.CreateDirectory(UE4BuildPath);
Directory.CreateDirectory(PAKFileDestination);
Console.WriteLine("PAK file exists...");
var PakFiles = Directory.EnumerateFiles(PAKFileLocation, "*.pak", SearchOption.TopDirectoryOnly);
foreach (var Name in PakFiles)
{
Console.WriteLine("Found file {0}", Name);
}
if (PakFiles.Count() > 0)
{
var DestFileName = Path.Combine(PAKFileDestination, Path.GetFileName(PakFiles.ElementAt(0)) + ".png"); // Need a rename to turn off compression
var SrcFileName = PakFiles.ElementAt(0);
if (!File.Exists(DestFileName) || File.GetLastWriteTimeUtc(DestFileName) < File.GetLastWriteTimeUtc(SrcFileName))
{
Console.WriteLine("Copying {0} to {1}", SrcFileName, DestFileName);
File.Copy(SrcFileName, DestFileName);
}
}
}
// Do we want to kill the OBB here or not???
}
//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(UE4BuildFilesPath, UE4BuildPath, Replacements);
CopyFileDirectory(UE4BuildFilesPath + "/NotForLicensees", UE4BuildPath, Replacements);
CopyFileDirectory(UE4BuildFilesPath + "/NoRedist", UE4BuildPath, Replacements);
CopyFileDirectory(GameBuildFilesPath, UE4BuildPath, Replacements);
CopyFileDirectory(GameBuildFilesPath + "/NotForLicensees", UE4BuildPath, Replacements);
CopyFileDirectory(GameBuildFilesPath + "/NoRedist", UE4BuildPath, Replacements);
// update metadata files (like project.properties, build.xml) if we are missing a build.xml or if we just overwrote project.properties with a bad version in it (from game/engine dir)
UpdateProjectProperties(UE4BuildPath, ProjectName);
// now make the apk(s)
string FinalNdkBuildABICommand = "";
for (int ArchIndex = 0; ArchIndex < Arches.Length; ArchIndex++)
{
string Arch = Arches[ArchIndex];
for (int GPUArchIndex = 0; GPUArchIndex < GPUArchitectures.Length; GPUArchIndex++)
{
Log.TraceInformation("\n===={0}====PREPARING NATIVE CODE=================================================================", DateTime.Now.ToString());
string GPUArchitecture = GPUArchitectures[GPUArchIndex];
string SourceSOName = AndroidToolChain.InlineArchName(OutputPath, Arch, GPUArchitecture);
// if the source binary was UE4Game, replace it with the new project name, when re-packaging a binary only build
string ApkFilename = Path.GetFileNameWithoutExtension(OutputPath).Replace("UE4Game", 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, GPUArchitecture);
}
// Copy the generated .so file from the binaries directory to the jni folder
if (!File.Exists(SourceSOName))
{
throw new BuildException("Can't make an APK without the compiled .so [{0}]", SourceSOName);
}
if (!Directory.Exists(UE4BuildPath + "/jni"))
{
throw new BuildException("Can't make an APK without the jni directory [{0}/jni]", UE4BuildFilesPath);
}
// Use ndk-build to do stuff and move the .so file to the lib folder (only if NDK is installed)
string FinalSOName = "";
if (File.Exists(NDKBuildPath))
{
string LibDir = UE4BuildPath + "/jni/" + GetNDKArch(Arch);
Directory.CreateDirectory(LibDir);
// copy the binary to the standard .so location
FinalSOName = LibDir + "/libUE4.so";
File.Copy(SourceSOName, FinalSOName, true);
FinalNdkBuildABICommand += GetNDKArch(Arch) + " ";
}
else
{
// if no NDK, we don't need any of the debugger stuff, so we just copy the .so to where it will end up
FinalSOName = UE4BuildPath + "/libs/" + GetNDKArch(Arch) + "/libUE4.so";
Directory.CreateDirectory(Path.GetDirectoryName(FinalSOName));
File.Copy(SourceSOName, FinalSOName);
}
// now do final stuff per apk (or after all .so's for a shared .apk)
if (bMakeSeparateApks || ArchIndex == NumArches - 1)
{
// always delete libs up to this point so fat binaries and incremental builds work together (otherwise we might end up with multiple
// so files in an apk that doesn't want them)
// note that we don't want to delete all libs, just the ones we copied (
foreach (string Lib in Directory.EnumerateFiles(UE4BuildPath + "/libs", "libUE4*.so", SearchOption.AllDirectories))
{
File.Delete(Lib);
}
// if we need to run ndk-build, do it now (if making a shared .apk, we need to wait until all .libs exist)
if (!string.IsNullOrEmpty(FinalNdkBuildABICommand))
{
string CommandLine = "APP_ABI=\"" + FinalNdkBuildABICommand + "\"";
if (!bForDistribution)
{
CommandLine += " NDK_DEBUG=1";
}
RunCommandLineProgramAndThrowOnError(UE4BuildPath, NDKBuildPath, CommandLine, "Preparing native code for debugging...", true);
// next loop we don't want to redo these ABIs
FinalNdkBuildABICommand = "";
}
// after ndk-build is called, we can now copy in the stl .so (ndk-build deletes old files)
// copy libgnustl_shared.so to library (use 4.8 if possible, otherwise 4.6)
if (bMakeSeparateApks)
{
CopySTL(UE4BuildPath, Arch);
CopyGfxDebugger(UE4BuildPath, Arch);
}
else
{
foreach (string InnerArch in Arches)
{
CopySTL(UE4BuildPath, InnerArch);
CopyGfxDebugger(UE4BuildPath, InnerArch);
}
}
// remove any read only flags
FileInfo DestFileInfo = new FileInfo(FinalSOName);
DestFileInfo.Attributes = DestFileInfo.Attributes & ~FileAttributes.ReadOnly;
Log.TraceInformation("\n===={0}====PERFORMING FINAL APK PACKAGE OPERATION================================================", DateTime.Now.ToString());
string AntBuildType = "debug";
string AntOutputSuffix = "-debug";
if (bForDistribution)
{
// this will write out ant.properties with info needed to sign a distribution build
PrepareToSignApk(UE4BuildPath);
AntBuildType = "release";
AntOutputSuffix = "-release";
}
// Use ant to build the .apk file
RunCommandLineProgramAndThrowOnError(UE4BuildPath, "cmd.exe", "/c \"" + GetAntPath() + "\" -quiet " + AntBuildType, "Making .apk with Ant... (note: it's safe to ignore javac obsolete warnings)");
// make sure destination exists
Directory.CreateDirectory(Path.GetDirectoryName(DestApkName));
// now copy to the final location
File.Copy(UE4BuildPath + "/bin/" + ProjectName + AntOutputSuffix + ".apk", DestApkName, true);
}
}
}
}
private void PrepareToSignApk(string BuildPath)
{
string ConfigFilePath = Path.Combine(BuildPath, "SigningConfig.xml");
if (!File.Exists(ConfigFilePath))
{
throw new BuildException("Unable to sign for Shipping without signing config file: '{0}", ConfigFilePath);
}
// open an Xml parser for the config file
string ConfigFile = File.ReadAllText(ConfigFilePath);
XmlReader Xml = XmlReader.Create(new StringReader(ConfigFile));
string Alias = "UESigningKey";
string KeystorePassword = "";
string KeyPassword = "_sameaskeystore_";
string Keystore = "UE.keystore";
Xml.ReadToFollowing("ue-signing-config");
bool bFinishedSection = false;
while (Xml.Read() && !bFinishedSection)
{
switch (Xml.NodeType)
{
case XmlNodeType.Element:
if (Xml.Name == "keyalias")
{
Alias = Xml.ReadElementContentAsString();
}
else if (Xml.Name == "keystorepassword")
{
KeystorePassword = Xml.ReadElementContentAsString();
}
else if (Xml.Name == "keypassword")
{
KeyPassword = Xml.ReadElementContentAsString();
}
else if (Xml.Name == "keystore")
{
Keystore = Xml.ReadElementContentAsString();
}
break;
case XmlNodeType.EndElement:
if (Xml.Name == "ue-signing-config")
{
bFinishedSection = true;
}
break;
}
}
string[] AntPropertiesLines = new string[4];
AntPropertiesLines[0] = "key.store=" + Keystore;
AntPropertiesLines[1] = "key.alias=" + Alias;
AntPropertiesLines[2] = "key.store.password=" + KeystorePassword;
AntPropertiesLines[3] = "key.alias.password=" + ((KeyPassword == "_sameaskeystore_") ? KeystorePassword : KeyPassword);
// now write out the properties
File.WriteAllLines(Path.Combine(BuildPath, "ant.properties"), AntPropertiesLines);
}
public override bool PrepTargetForDeployment(UEBuildTarget InTarget)
{
// we need to strip architecture from any of the output paths
string BaseSoName = AndroidToolChain.RemoveArchName(InTarget.OutputPaths[0]);
// this always makes a merged .apk since for debugging, there's no way to know which one to run
MakeApk(InTarget.AppName, InTarget.ProjectDirectory, BaseSoName, BuildConfiguration.RelativeEnginePath, bForDistribution:false, CookFlavor:"", bMakeSeparateApks:false, bIncrementalPackage:true);
return true;
}
public static bool ShouldMakeSeparateApks()
{
// check to see if the project wants separate apks
ConfigCacheIni Ini = new ConfigCacheIni(UnrealTargetPlatform.Android, "Engine", UnrealBuildTool.GetUProjectPath());
bool bSeparateApks = false;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bSplitIntoSeparateApks", out bSeparateApks);
return bSeparateApks;
}
public override bool PrepForUATPackageOrDeploy(string ProjectName, string ProjectDirectory, string ExecutablePath, string EngineDirectory, bool bForDistribution, string CookFlavor)
{
MakeApk(ProjectName, ProjectDirectory, ExecutablePath, EngineDirectory, bForDistribution, CookFlavor, ShouldMakeSeparateApks(), bIncrementalPackage:false);
return true;
}
public static void OutputReceivedDataEventHandler(Object Sender, DataReceivedEventArgs Line)
{
if ((Line != null) && (Line.Data != null))
{
Log.TraceInformation(Line.Data);
}
}
}
}