Files
UnrealEngine/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/Android/Gauntlet.AndroidBuildSource.cs
2025-05-18 13:04:45 +08:00

273 lines
8.6 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using AutomationTool;
using UnrealBuildTool;
using System.Threading;
using System.Text.RegularExpressions;
using System.Linq;
using EpicGames.Core;
namespace Gauntlet
{
public class AndroidBuild : IBuild
{
public int PreferenceOrder { get { return 0; } }
public UnrealTargetConfiguration Configuration { get; protected set; }
public string SourceApkPath;
public Dictionary<string, string> FilesToInstall;
public string AndroidPackageName;
public BuildFlags Flags { get; protected set; }
public string Flavor { get { return ""; } }
public UnrealTargetPlatform Platform { get { return UnrealTargetPlatform.Android; } }
public bool SupportsAdditionalFileCopy => true;
public bool Is32Bit { get; protected set; }
public bool UsesExternalFilesDir { get; protected set; }
public bool UsesPublicLogs { get; protected set; }
public AndroidBuild(UnrealTargetConfiguration InConfig, string InAndroidPackageName, string InApkPath, Dictionary<string, string> InFilesToInstall, BuildFlags InFlags, bool InIs32Bit, bool bInUsesExternalFilesDir, bool bInUsesPublicLogs)
{
Configuration = InConfig;
AndroidPackageName = InAndroidPackageName;
SourceApkPath = InApkPath;
FilesToInstall = InFilesToInstall;
Flags = InFlags;
Is32Bit = InIs32Bit;
UsesExternalFilesDir = bInUsesExternalFilesDir;
UsesPublicLogs = bInUsesPublicLogs;
}
public bool CanSupportRole(UnrealTargetRole RoleType)
{
if (RoleType.IsClient())
{
return true;
}
return false;
}
public static IEnumerable<AndroidBuild> CreateFromPath(string InProjectName, string InPath)
{
string BuildPath = InPath;
List<AndroidBuild> DiscoveredBuilds = new List<AndroidBuild>();
DirectoryInfo Di = new DirectoryInfo(BuildPath);
// find all install batchfiles
FileInfo[] InstallFiles = Di.GetFiles("Install_*");
foreach (FileInfo Fi in InstallFiles)
{
bool PackageIs32Bit = Fi.FullName.Contains("armv7");
UnrealTargetConfiguration UnrealConfig = UnrealHelpers.GetConfigurationFromExecutableName(InProjectName, Fi.FullName);
UnrealTargetRole UnrealRole = UnrealHelpers.GetRoleFromExecutableName(InProjectName, Fi.FullName);
if (UnrealConfig == UnrealTargetConfiguration.Unknown)
{
Log.Info("Skipping unrecognized build {0}", Fi.FullName);
continue;
}
bool TestInstall = Fi.Name.EndsWith("_TEST.bat", StringComparison.OrdinalIgnoreCase);
bool PatchInstall = Fi.Name.EndsWith("_Patch.bat", StringComparison.OrdinalIgnoreCase);
// filter out non-matching or test installation batch files
// test installation scripts are intended to be manually invoked
if (TestInstall || PatchInstall)
{
if (TestInstall || PatchInstall)
{
Log.Verbose("Ignoring {0} installation batch file {1}", TestInstall ? "test" : "patch", Fi.Name);
}
continue;
}
Log.Verbose("Pulling install data from {0}", Fi.FullName);
string AbsPath = Fi.Directory.FullName;
// read contents and replace linefeeds (regex doesn't stop on them :((
string BatContents = File.ReadAllText(Fi.FullName).Replace(Environment.NewLine, "\n");
// Replace .bat with .apk and strip up to and including the first _, that is then our APK name
var SourceApkMatch = Regex.Match(BatContents, @" install\s+(.+\.apk)");
if ( SourceApkMatch.Groups.Count <= 0)
{
Log.Warning(KnownLogEvents.Gauntlet_BuildDropEvent, "Could not parse install command from {File}", Fi.FullName);
continue;
}
string SourceApkPath = Path.Combine(AbsPath,SourceApkMatch.Groups[1].ToString());
bool bUsesExternalFilesDir = false;
bool bUsesPublicLogs = false;
// parse APK's metadata for some build details
{
AndroidPlatform.GetPackageInfo(SourceApkPath, false);
// Establish remote directory usage
string ApkUsesExternalFilesDir = AndroidPlatform.GetMetadataValue("bUseExternalFilesDir");
string ApkUsesPublicLogs = AndroidPlatform.GetMetadataValue("bPublicLogFiles");
bUsesExternalFilesDir = ApkUsesExternalFilesDir != null ? ApkUsesExternalFilesDir.Contains("1") : false;
bUsesPublicLogs = ApkUsesPublicLogs != null ? ApkUsesPublicLogs.Contains("1") : false;
}
// save com.companyname.product
string AndroidPackageName = Regex.Match(BatContents, @"uninstall\s+(com\..+)").Groups[1].ToString();
// pull all OBBs (probably just one..)
var OBBMatches = Regex.Matches(BatContents, @"push\s+(.+?)\s+(.+)");
// save them as a dict of full paths as keys and dest paths as values
Dictionary<string, string> FilesToInstall = OBBMatches.Cast<Match>().ToDictionary(M => Path.Combine(AbsPath, M.Groups[1].ToString()), M => M.Groups[2].ToString());
if (string.IsNullOrEmpty(SourceApkPath))
{
Log.Warning(KnownLogEvents.Gauntlet_BuildDropEvent, "No APK found for build at {File}", Fi.FullName);
continue;
}
if (!File.Exists(SourceApkPath))
{
Log.Warning(KnownLogEvents.Gauntlet_BuildDropEvent, "Resolved APK name but it doesn't exist {File}", SourceApkPath);
continue;
}
if (string.IsNullOrEmpty(AndroidPackageName))
{
Log.Warning(KnownLogEvents.Gauntlet_BuildDropEvent, "No product name found for build at {File}", Fi.FullName);
continue;
}
// Android builds are always packaged, and we can always replace the command line
BuildFlags Flags = BuildFlags.Packaged | BuildFlags.CanReplaceCommandLine;
// if there's data then the pak files are in an obb and we can sub in a new exe
if (FilesToInstall.Count() > 0)
{
Flags |= BuildFlags.CanReplaceExecutable;
}
if (AbsPath.Contains("Bulk"))
{
Flags |= BuildFlags.Bulk;
}
else
{
Flags |= BuildFlags.NotBulk;
}
AndroidBuild NewBuild = new AndroidBuild(UnrealConfig, AndroidPackageName, SourceApkPath, FilesToInstall, Flags, PackageIs32Bit, bUsesExternalFilesDir, bUsesPublicLogs);
DiscoveredBuilds.Add(NewBuild);
Log.Verbose("Found {0} {1} build at {2}", UnrealConfig, ((Flags & BuildFlags.Bulk) == BuildFlags.Bulk) ? "(bulk)" : "(not bulk)", AbsPath);
}
// If we have both 32 and 64-bit builds, prefer 64-bit
if (DiscoveredBuilds.Where(B => B.Is32Bit == false).Any())
{
DiscoveredBuilds = DiscoveredBuilds.Where(B => !B.Is32Bit).ToList();
}
return DiscoveredBuilds;
}
}
public class AndroidBuildSource : IFolderBuildSource
{
public string BuildName { get { return "AndroidBuildSource"; } }
public bool CanSupportPlatform(UnrealTargetPlatform InPlatform)
{
return InPlatform == UnrealTargetPlatform.Android;
}
public string ProjectName { get; protected set; }
public AndroidBuildSource()
{
}
public List<IBuild> GetBuildsAtPath(string InProjectName, string InPath, int MaxRecursion = 3)
{
List<DirectoryInfo> AllDirs = new List<DirectoryInfo>();
//AndroidBuildSource BuildSource = null;
List<IBuild> Builds = new List<IBuild>();
// c:\path\to\build
DirectoryInfo PathDI = new DirectoryInfo(InPath);
if (PathDI.Exists)
{
if (PathDI.Name.IndexOf("Android", StringComparison.OrdinalIgnoreCase) >= 0)
{
AllDirs.Add(PathDI);
}
// find all directories that begin with Android
DirectoryInfo[] AndroidDirs = PathDI.GetDirectories("Android*", SearchOption.TopDirectoryOnly);
AllDirs.AddRange(AndroidDirs);
List<DirectoryInfo> DirsToRecurse = AllDirs;
// now get subdirs
while (MaxRecursion-- > 0)
{
List<DirectoryInfo> DiscoveredDirs = new List<DirectoryInfo>();
DirsToRecurse.ToList().ForEach((D) =>
{
DiscoveredDirs.AddRange(D.GetDirectories("*", SearchOption.TopDirectoryOnly));
});
AllDirs.AddRange(DiscoveredDirs);
DirsToRecurse = DiscoveredDirs;
}
string AndroidBuildFilter = Globals.Params.ParseValue("AndroidBuildFilter", "");
foreach (DirectoryInfo Di in AllDirs)
{
IEnumerable<AndroidBuild> FoundBuilds = AndroidBuild.CreateFromPath(InProjectName, Di.FullName);
if (FoundBuilds != null)
{
if (!string.IsNullOrEmpty(AndroidBuildFilter))
{
//IndexOf used because Contains must be case-sensitive
FoundBuilds = FoundBuilds.Where(B => B.SourceApkPath.IndexOf(AndroidBuildFilter, StringComparison.OrdinalIgnoreCase) >= 0);
}
Builds.AddRange(FoundBuilds);
}
}
}
return Builds;
}
/*public AndroidBuild GetBuild(UnrealTargetConfiguration InConfig, BuildFlags InFlags)
{
return Builds.Where((B) => {
return B.Configuration == InConfig && (B.Flags & InFlags) > 0;
}).FirstOrDefault();
}*/
}
}