Files
2025-05-18 13:04:45 +08:00

637 lines
21 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
namespace UnrealBuildTool
{
class AndroidAARHandler
{
public class AndroidAAREntry
{
public string BaseName;
public string Version;
public string Filename;
public List<string> Dependencies;
public AndroidAAREntry(string InBaseName, string InVersion, string InFilename)
{
BaseName = InBaseName;
Version = InVersion;
Filename = InFilename;
Dependencies = new List<string>();
}
public void AddDependency(string InBaseName, string InVersion)
{
Dependencies.Add(InBaseName); // + "-" + InVersion); will replace version with latest
}
public void ClearDependencies()
{
Dependencies.Clear();
}
}
public List<string>? Repositories = null;
public List<AndroidAAREntry>? AARList = null;
private List<AndroidAAREntry>? JARList = null;
/// <summary>
/// Handler for AAR and JAR dependency determination and staging
/// </summary>
public AndroidAARHandler()
{
Repositories = new List<string>();
AARList = new List<AndroidAAREntry>();
JARList = new List<AndroidAAREntry>();
}
/// <summary>
/// Add a new respository path to search for AAR and JAR files
/// </summary>
/// <param name="RepositoryPath">Directory containing the repository</param>
/// <param name="Logger">Logger for output</param>
public void AddRepository(string RepositoryPath, ILogger Logger)
{
if (Directory.Exists(RepositoryPath))
{
Logger.LogInformation("Added repository: {RepositoryPath}", RepositoryPath);
Repositories!.Add(RepositoryPath);
}
else
{
Logger.LogWarning("AddRepository: Directory {RepositoryPath} not found!", RepositoryPath);
}
}
/// <summary>
/// Add new respository paths to search for AAR and JAR files (recursive)
/// </summary>
/// <param name="RepositoryPath">Root directory containing the repository</param>
/// <param name="SearchPattern">Search pattern to match</param>
/// <param name="Logger">Logger for output</param>
public void AddRepositories(string RepositoryPath, string SearchPattern, ILogger Logger)
{
if (Directory.Exists(RepositoryPath))
{
List<string> ToCheck = new List<string>();
ToCheck.Add(RepositoryPath);
while (ToCheck.Count > 0)
{
int LastIndex = ToCheck.Count - 1;
string CurrentDir = ToCheck[LastIndex];
ToCheck.RemoveAt(LastIndex);
foreach (string SearchPath in Directory.GetDirectories(CurrentDir))
{
if (SearchPath.Contains(SearchPattern))
{
Logger.LogInformation("Added repository: {SearchPath}", SearchPath);
Repositories!.Add(SearchPath);
}
else
{
ToCheck.Add(SearchPath);
}
}
}
}
else
{
Logger.LogInformation("AddRepositories: Directory {RepositoryPath} not found; ignored", RepositoryPath);
}
}
public void DumpAAR(ILogger Logger)
{
Logger.LogInformation("ALL DEPENDENCIES");
foreach (AndroidAAREntry Entry in AARList!)
{
Logger.LogInformation("{EntryFilename}", Entry.Filename);
}
foreach (AndroidAAREntry Entry in JARList!)
{
Logger.LogInformation("{EntryFilename}", Entry.Filename);
}
}
private string GetElementValue(XElement SourceElement, XName ElementName, string DefaultValue)
{
XElement? Element = SourceElement.Element(ElementName);
return (Element != null) ? Element.Value : DefaultValue;
}
private string? FindPackageFile(string PackageName, string BaseName, string Version)
{
string[] Sections = PackageName.Split('.');
string PackagePath = Path.Combine(Sections);
foreach (string Repository in Repositories!)
{
string PackageDirectory = Path.Combine(Repository, PackagePath, BaseName, Version);
if (Directory.Exists(PackageDirectory))
{
return PackageDirectory;
}
}
return null;
}
private bool HasAnyVersionCharacters(string InValue)
{
for (int Index = 0; Index < InValue.Length; Index++)
{
char c = InValue[Index];
if (c == '.' || (c >= '0' && c <= '9'))
{
return true;
}
}
return false;
}
private bool HasOnlyVersionCharacters(string InValue)
{
for (int Index = 0; Index < InValue.Length; Index++)
{
char c = InValue[Index];
if (c != '.' && !(c >= '0' && c <= '9'))
{
return false;
}
}
return true;
}
private uint GetVersionValue(string VersionString)
{
// 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) ? (UInt32.Parse(Sections[0]) << 24) : 0;
Value |= (Sections.Length > 1) ? (UInt32.Parse(Sections[1]) << 16) : 0;
Value |= (Sections.Length > 2) ? (UInt32.Parse(Sections[2]) << 8) : 0;
Value |= (Sections.Length > 3) ? UInt32.Parse(Sections[3]) : 0;
}
catch (Exception)
{
// ignore poorly formed version
}
return Value;
}
// clean up the version (Maven version info here: https://docs.oracle.com/middleware/1212/core/MAVEN/maven_version.htm)
// only going to handle a few cases, not proper ranges (keeps the rightmost valid version which should be highest)
// will still return something but will include an error in log, but don't want to throw an exception
private string CleanupVersion(string Filename, string InVersion, ILogger Logger)
{
string WorkVersion = InVersion;
// if has commas, keep the rightmost part with actual numbers
if (WorkVersion.Contains(','))
{
string[] CommaParts = WorkVersion.Split(',');
WorkVersion = "";
for (int Index = CommaParts.Length - 1; Index >= 0; Index--)
{
if (HasAnyVersionCharacters(CommaParts[Index]))
{
WorkVersion = CommaParts[Index];
break;
}
}
}
// if not left with a possibly valid number, stop
if (!HasAnyVersionCharacters(WorkVersion))
{
Logger.LogError("AAR Dependency file {Filename} version unknown! {InVersion}", Filename, InVersion);
return InVersion;
}
// just remove any parens or brackets left
WorkVersion = WorkVersion.Replace("(", "").Replace(")", "").Replace("[", "").Replace("]", "");
// just return it if now looks value
if (HasOnlyVersionCharacters(WorkVersion))
{
return WorkVersion;
}
// give an error, not likely going to work, though
Logger.LogError("AAR Dependency file {Filename} version unknown! {InVersion}", Filename, InVersion);
return InVersion;
}
/// <summary>
/// Adds a new required JAR file and resolves dependencies
/// </summary>
/// <param name="PackageName">Name of the package the JAR belongs to in repository</param>
/// <param name="BaseName">Directory in repository containing the JAR</param>
/// <param name="Version">Version of the AAR to use</param>
/// <param name="Logger">Logger instance</param>
public void AddNewJAR(string PackageName, string BaseName, string Version, ILogger Logger)
{
string? BasePath = FindPackageFile(PackageName, BaseName, Version);
if (BasePath == null)
{
Logger.LogError("AAR: Unable to find package {Package}!", PackageName + "/" + BaseName);
return;
}
string BaseFilename = Path.Combine(BasePath, BaseName + "-" + Version);
// Check if already added
uint NewVersionValue = GetVersionValue(Version);
for (int JARIndex = 0; JARIndex < JARList!.Count; JARIndex++)
{
if (JARList[JARIndex].BaseName == BaseName)
{
// Is it the same version or older? ignore if so
uint EntryVersionValue = GetVersionValue(JARList[JARIndex].Version);
if (NewVersionValue <= EntryVersionValue)
{
return;
}
Logger.LogInformation("AAR: {BaseName}: {Version1} newer than {Version2}", JARList[JARIndex].BaseName, Version, JARList[JARIndex].Version);
// This is a newer version; remove old one
JARList.RemoveAt(JARIndex);
break;
}
}
//Logger.LogInformation("JAR: {BaseName}", BaseName);
AndroidAAREntry AAREntry = new AndroidAAREntry(BaseName, Version, BaseFilename);
JARList.Add(AAREntry);
// Check for dependencies
XDocument DependsXML;
string DependencyFilename = BaseFilename + ".pom";
if (File.Exists(DependencyFilename))
{
try
{
DependsXML = XDocument.Load(DependencyFilename);
}
catch (Exception e)
{
Logger.LogError("AAR Dependency file {File} parsing error! {Ex}", DependencyFilename, e);
return;
}
}
else
{
Logger.LogError("AAR: Dependency file {DependencyFilename} missing!", DependencyFilename);
return;
}
string NameSpace = DependsXML.Root!.Name.NamespaceName;
XName DependencyName = XName.Get("dependency", NameSpace);
XName GroupIdName = XName.Get("groupId", NameSpace);
XName ArtifactIdName = XName.Get("artifactId", NameSpace);
XName VersionName = XName.Get("version", NameSpace);
XName ScopeName = XName.Get("scope", NameSpace);
XName TypeName = XName.Get("type", NameSpace);
foreach (XElement DependNode in DependsXML.Descendants(DependencyName))
{
string DepGroupId = GetElementValue(DependNode, GroupIdName, "");
string DepArtifactId = GetElementValue(DependNode, ArtifactIdName, "");
string DepVersion = CleanupVersion(DependencyFilename + "." + DepGroupId + "." + DepArtifactId, GetElementValue(DependNode, VersionName, ""), Logger);
string DepScope = GetElementValue(DependNode, ScopeName, "compile");
string DepType = GetElementValue(DependNode, TypeName, "jar");
//Logger.LogInformation("Dependency: {DepGroupId} {DepArtifactId} {DepVersion} {DepScope} {DepType}", DepGroupId, DepArtifactId, DepVersion, DepScope, DepType);
// ignore test scope
if (DepScope == "test")
{
continue;
}
if (DepType == "aar")
{
AddNewAAR(DepGroupId, DepArtifactId, DepVersion, Logger);
}
else if (DepType == "jar")
{
AddNewJAR(DepGroupId, DepArtifactId, DepVersion, Logger);
}
}
}
/// <summary>
/// Adds a new required AAR file and resolves dependencies
/// </summary>
/// <param name="PackageName">Name of the package the AAR belongs to in repository</param>
/// <param name="BaseName">Directory in repository containing the AAR</param>
/// <param name="Version">Version of the AAR to use</param>
/// <param name="Logger">Logger for output</param>
/// <param name="HandleDependencies">Optionally process POM file for dependencies (default)</param>
public void AddNewAAR(string PackageName, string BaseName, string Version, ILogger Logger, bool HandleDependencies = true)
{
if (!HandleDependencies)
{
AndroidAAREntry NewAAREntry = new AndroidAAREntry(BaseName, Version, PackageName);
AARList!.Add(NewAAREntry);
return;
}
string? BasePath = FindPackageFile(PackageName, BaseName, Version);
if (BasePath == null)
{
Logger.LogError("AAR: Unable to find package {Package}!", PackageName + "/" + BaseName);
return;
}
string BaseFilename = Path.Combine(BasePath, BaseName + "-" + Version);
// Check if already added
uint NewVersionValue = GetVersionValue(Version);
for (int AARIndex = 0; AARIndex < AARList!.Count; AARIndex++)
{
if (AARList[AARIndex].BaseName == BaseName)
{
// Is it the same version or older? ignore if so
uint EntryVersionValue = GetVersionValue(AARList[AARIndex].Version);
if (NewVersionValue <= EntryVersionValue)
{
return;
}
Logger.LogInformation("AAR: {BaseName}: {Version1} newer than {Version2}", AARList[AARIndex].BaseName, Version, AARList[AARIndex].Version);
// This is a newer version; remove old one
// @TODO: be smarter about dependency cleanup (newer AAR might not need older dependencies)
AARList.RemoveAt(AARIndex);
break;
}
}
//Logger.LogInformation("AAR: {BaseName}", BaseName);
AndroidAAREntry AAREntry = new AndroidAAREntry(BaseName, Version, BaseFilename);
AARList.Add(AAREntry);
if (!HandleDependencies)
{
return;
}
// Check for dependencies
XDocument DependsXML;
string DependencyFilename = BaseFilename + ".pom";
if (File.Exists(DependencyFilename))
{
try
{
DependsXML = XDocument.Load(DependencyFilename);
}
catch (Exception e)
{
Logger.LogError("AAR Dependency file {File} parsing error! {Ex}", DependencyFilename, e);
return;
}
}
else
{
Logger.LogError("AAR: Dependency file {DependencyFilename} missing!", DependencyFilename);
return;
}
string NameSpace = DependsXML.Root!.Name.NamespaceName;
XName DependencyName = XName.Get("dependency", NameSpace);
XName GroupIdName = XName.Get("groupId", NameSpace);
XName ArtifactIdName = XName.Get("artifactId", NameSpace);
XName VersionName = XName.Get("version", NameSpace);
XName ScopeName = XName.Get("scope", NameSpace);
XName TypeName = XName.Get("type", NameSpace);
foreach (XElement DependNode in DependsXML.Descendants(DependencyName))
{
string DepGroupId = GetElementValue(DependNode, GroupIdName, "");
string DepArtifactId = GetElementValue(DependNode, ArtifactIdName, "");
string DepVersion = CleanupVersion(DependencyFilename + "." + DepGroupId + "." + DepArtifactId, GetElementValue(DependNode, VersionName, ""), Logger);
string DepScope = GetElementValue(DependNode, ScopeName, "compile");
string DepType = GetElementValue(DependNode, TypeName, "jar");
//Logger.LogInformation("Dependency: {DepGroupId} {DepArtifactId} {DepVersion} {DepScope} {DepType}", DepGroupId, DepArtifactId, DepVersion, DepScope, DepType);
// ignore test scope
if (DepScope == "test")
{
continue;
}
if (DepType == "aar")
{
// Add dependency
AAREntry.AddDependency(DepArtifactId, DepVersion);
AddNewAAR(DepGroupId, DepArtifactId, DepVersion, Logger);
}
else
if (DepType == "jar")
{
AddNewJAR(DepGroupId, DepArtifactId, DepVersion, Logger);
}
}
}
private void MakeDirectoryIfRequiredForFile(string DestFilename)
{
string DestSubdir = Path.GetDirectoryName(DestFilename)!;
if (!Directory.Exists(DestSubdir))
{
Directory.CreateDirectory(DestSubdir);
}
}
private void MakeDirectoryIfRequired(string DestDirectory)
{
if (!Directory.Exists(DestDirectory))
{
Directory.CreateDirectory(DestDirectory);
}
}
/// <summary>
/// Copies the required JAR files to the provided directory
/// </summary>
/// <param name="DestinationPath">Destination path for JAR files</param>
/// <param name="Logger">Logger for output</param>
public void CopyJARs(string DestinationPath, ILogger Logger)
{
MakeDirectoryIfRequired(DestinationPath);
DestinationPath = Path.Combine(DestinationPath, "libs");
MakeDirectoryIfRequired(DestinationPath);
foreach (AndroidAAREntry Entry in JARList!)
{
string Filename = Entry.Filename + ".jar";
string BaseName = Path.GetFileName(Filename);
string TargetPath = Path.Combine(DestinationPath, BaseName);
//Logger.LogInformation("Attempting to copy JAR {Filename} {BaseName} {TargetPath}", Filename, BaseName, TargetPath);
if (!File.Exists(Filename))
{
Logger.LogInformation("JAR doesn't exist! {Filename}", Filename);
}
if (!File.Exists(TargetPath))
{
Logger.LogInformation("Copying JAR {BaseName}", BaseName);
File.Copy(Filename, TargetPath);
}
}
}
/// <summary>
/// Extracts the required AAR files to the provided directory
/// </summary>
/// <param name="DestinationPath">Destination path for AAR files</param>
/// <param name="AppPackageName">Name of the package these AARs are being used with</param>
/// <param name="Logger">Logger for output</param>
public void ExtractAARs(string DestinationPath, string AppPackageName, ILogger Logger)
{
MakeDirectoryIfRequired(DestinationPath);
DestinationPath = Path.Combine(DestinationPath, "JavaLibs");
MakeDirectoryIfRequired(DestinationPath);
Logger.LogInformation("Extracting AARs");
foreach (AndroidAAREntry Entry in AARList!)
{
string BaseName = Path.GetFileName(Entry.Filename);
string TargetPath = Path.Combine(DestinationPath, BaseName);
// Only extract if haven't before to prevent changing timestamps
string TargetManifestFileName = Path.Combine(TargetPath, "AndroidManifest.xml");
if (!File.Exists(TargetManifestFileName))
{
Logger.LogInformation("Extracting AAR {BaseName}", BaseName);
/*IEnumerable<string> FileNames =*/
UnzipFiles(Entry.Filename + ".aar", TargetPath, Logger);
// Must have a src directory (even if empty)
string SrcDirectory = Path.Combine(TargetPath, "src");
if (!Directory.Exists(SrcDirectory))
{
Directory.CreateDirectory(SrcDirectory);
//FileStream Placeholder = new FileStream(Path.Combine(SrcDirectory, ".placeholder_" + BaseName), FileMode.Create, FileAccess.Write);
}
// Move the class.jar file
string ClassSourceFile = Path.Combine(TargetPath, "classes.jar");
if (File.Exists(ClassSourceFile))
{
string ClassDestFile = Path.Combine(TargetPath, "libs", BaseName + ".jar");
MakeDirectoryIfRequiredForFile(ClassDestFile);
File.Move(ClassSourceFile, ClassDestFile);
}
// Now create the project.properties file
string PropertyFilename = Path.Combine(TargetPath, "project.properties");
if (!File.Exists(PropertyFilename))
{
// Try to get the SDK target from the AndroidManifest.xml
string MinSDK = "9";
string ManifestFilename = Path.Combine(TargetPath, "AndroidManifest.xml");
XDocument ManifestXML;
if (File.Exists(ManifestFilename))
{
try
{
// Replace all instances of the application id with the packagename
string Contents = File.ReadAllText(ManifestFilename);
string NewContents = Contents.Replace("${applicationId}", AppPackageName);
if (Contents != NewContents)
{
File.WriteAllText(ManifestFilename, NewContents);
}
ManifestXML = XDocument.Load(ManifestFilename);
XElement UsesSdk = ManifestXML.Root!.Element(XName.Get("uses-sdk", ManifestXML.Root.Name.NamespaceName))!;
XAttribute Target = UsesSdk.Attribute(XName.Get("minSdkVersion", "http://schemas.android.com/apk/res/android"))!;
MinSDK = Target.Value;
}
catch (Exception e)
{
Logger.LogError("AAR Manifest file {ManifestFile} parsing error! {Ex}", ManifestFilename, e);
}
}
// Project contents
string ProjectPropertiesContents = "target=android-" + MinSDK + "\nandroid.library=true\n";
// Add the dependencies
int RefCount = 0;
foreach (string DependencyName in Entry.Dependencies)
{
// Find the version
foreach (AndroidAAREntry ScanEntry in AARList)
{
if (ScanEntry.BaseName == DependencyName)
{
RefCount++;
ProjectPropertiesContents += "android.library.reference." + RefCount + "=../" + DependencyName + "-" + ScanEntry.Version + "\n";
break;
}
}
}
File.WriteAllText(PropertyFilename, ProjectPropertiesContents);
}
}
}
}
/// <summary>
/// Extracts the contents of a zip file
/// </summary>
/// <param name="ZipFileName">Name of the zip file</param>
/// <param name="BaseDirectory">Output directory</param>
/// <param name="Logger">Logger for output</param>
/// <returns>List of files written</returns>
public static IEnumerable<string> UnzipFiles(string ZipFileName, string BaseDirectory, ILogger Logger)
{
// manually extract the files. There was a problem with the Ionic.Zip library that required this on non-PC at one point,
// but that problem is now fixed. Leaving this code as is as we need to return the list of created files and fix up their permissions anyway.
using (Ionic.Zip.ZipFile Zip = new Ionic.Zip.ZipFile(ZipFileName))
{
List<string> OutputFileNames = new List<string>();
foreach (Ionic.Zip.ZipEntry Entry in Zip.Entries)
{
// support-v4 and support-v13 has the jar file named with "internal_impl-XX.X.X.jar"
// this causes error "Found 2 versions of internal_impl-XX.X.X.jar"
// following codes adds "support-v4-..." to the output jar file name to avoid the collision
string OutputFileName = Path.Combine(BaseDirectory, Entry.FileName);
if (Entry.FileName.Contains("internal_impl"))
{
string _ZipName = Path.GetFileNameWithoutExtension(ZipFileName);
string NewOutputFileName = Path.Combine(Path.GetDirectoryName(OutputFileName)!,
_ZipName + '-' + Path.GetFileNameWithoutExtension(OutputFileName) + '.' + Path.GetExtension(OutputFileName));
Logger.LogInformation("Changed FileName {EntryFileName} => {NewOutputFileName}", Entry.FileName, NewOutputFileName);
OutputFileName = NewOutputFileName;
}
Directory.CreateDirectory(Path.GetDirectoryName(OutputFileName)!);
if (!Entry.IsDirectory)
{
using (FileStream OutputStream = new FileStream(OutputFileName, FileMode.Create, FileAccess.Write))
{
Entry.Extract(OutputStream);
}
OutputFileNames.Add(OutputFileName);
}
}
return OutputFileNames;
}
}
}
}