// 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 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 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 CreateFromPath(string InProjectName, string InPath) { string BuildPath = InPath; List DiscoveredBuilds = new List(); 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 FilesToInstall = OBBMatches.Cast().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 GetBuildsAtPath(string InProjectName, string InPath, int MaxRecursion = 3) { List AllDirs = new List(); //AndroidBuildSource BuildSource = null; List Builds = new List(); // 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 DirsToRecurse = AllDirs; // now get subdirs while (MaxRecursion-- > 0) { List DiscoveredDirs = new List(); 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 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(); }*/ } }