// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; using System.Xml.Linq; using System.Diagnostics; using System.IO; using Ionic.Zip; namespace UnrealBuildTool { class AndroidAARHandler { class AndroidAAREntry { public string BaseName; public string Version; public string Filename; public List Dependencies; public AndroidAAREntry(string InBaseName, string InVersion, string InFilename) { BaseName = InBaseName; Version = InVersion; Filename = InFilename; Dependencies = new List(); } public void AddDependency(string InBaseName, string InVersion) { Dependencies.Add(InBaseName); // + "-" + InVersion); will replace version with latest } public void ClearDependencies() { Dependencies.Clear(); } } private List Repositories = null; private List AARList = null; private List JARList = null; /// /// Handler for AAR and JAR dependency determination and staging /// public AndroidAARHandler() { Repositories = new List(); AARList = new List(); JARList = new List(); } /// /// Add a new respository path to search for AAR and JAR files /// /// Directory containing the repository public void AddRepository(string RepositoryPath) { if (Directory.Exists(RepositoryPath)) { Log.TraceInformation("Added repository: {0}", RepositoryPath); Repositories.Add(RepositoryPath); } else { Log.TraceWarning("AddRepository: Directory {0} not found!", RepositoryPath); } } /// /// Add new respository paths to search for AAR and JAR files (recursive) /// /// Root directory containing the repository /// Search pattern to match public void AddRepositories(string RepositoryPath, string SearchPattern) { List ToCheck = new List(); 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)) { Log.TraceInformation("Added repository: {0}", SearchPath); Repositories.Add(SearchPath); } else { ToCheck.Add(SearchPath); } } } } public void DumpAAR() { Log.TraceInformation("ALL DEPENDENCIES"); foreach (AndroidAAREntry Entry in AARList) { Log.TraceInformation("{0}", Entry.Filename); } foreach (AndroidAAREntry Entry in JARList) { Log.TraceInformation("{0}", 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; } /// /// Adds a new required JAR file and resolves dependencies /// /// Name of the package the JAR belongs to in repository /// Directory in repository containing the JAR /// Version of the AAR to use public void AddNewJAR(string PackageName, string BaseName, string Version) { string BasePath = FindPackageFile(PackageName, BaseName, Version); if (BasePath == null) { Log.TraceError("AAR: Unable to find package {0}!", PackageName + "/" + BaseName); return; } string BaseFilename = Path.Combine(BasePath, BaseName + "-" + Version); // Check if already added for (int JARIndex = 0; JARIndex < JARList.Count; JARIndex++) { if (JARList[JARIndex].BaseName == BaseName) { // Is it the same version? if (JARList[JARIndex].Version == Version) { return; } // Ignore if older version string[] EntryVersionParts = JARList[JARIndex].Version.Split('.'); string[] NewVersionParts = Version.Split('.'); for (int Index = 0; Index < EntryVersionParts.Length; Index++) { int EntryVersionInt = 0; if (int.TryParse(EntryVersionParts[Index], out EntryVersionInt)) { int NewVersionInt = 0; if (int.TryParse(NewVersionParts[Index], out NewVersionInt)) { if (NewVersionInt < EntryVersionInt) { return; } } else { return; } } else { return; } } Log.TraceInformation("AAR: {0}: {1} newer than {2}", JARList[JARIndex].BaseName, Version, JARList[JARIndex].Version); // This is a newer version; remove old one JARList.RemoveAt(JARIndex); break; } } //Log.TraceInformation("JAR: {0}", 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) { Log.TraceError("AAR Dependency file {0} parsing error! {1}", DependencyFilename, e); return; } } else { Log.TraceError("AAR: Dependency file {0} 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 = GetElementValue(DependNode, VersionName, ""); string DepScope = GetElementValue(DependNode, ScopeName, "compile"); string DepType = GetElementValue(DependNode, TypeName, "jar"); //Log.TraceInformation("Dependency: {0} {1} {2} {3} {4}", DepGroupId, DepArtifactId, DepVersion, DepScope, DepType); // ignore test scope if (DepScope == "test") { continue; } if (DepType == "aar") { AddNewAAR(DepGroupId, DepArtifactId, DepVersion); } else if (DepType == "jar") { AddNewJAR(DepGroupId, DepArtifactId, DepVersion); } } } /// /// Adds a new required AAR file and resolves dependencies /// /// Name of the package the AAR belongs to in repository /// Directory in repository containing the AAR /// Version of the AAR to use public void AddNewAAR(string PackageName, string BaseName, string Version) { string BasePath = FindPackageFile(PackageName, BaseName, Version); if (BasePath == null) { Log.TraceError("AAR: Unable to find package {0}!", PackageName + "/" + BaseName); return; } string BaseFilename = Path.Combine(BasePath, BaseName + "-" + Version); // Check if already added for (int AARIndex = 0; AARIndex < AARList.Count; AARIndex++) { if (AARList[AARIndex].BaseName == BaseName) { // Is it the same version? if (AARList[AARIndex].Version == Version) { return; } // Ignore if older version string[] EntryVersionParts = AARList[AARIndex].Version.Split('.'); string[] NewVersionParts = Version.Split('.'); for (int Index = 0; Index < EntryVersionParts.Length; Index++) { int EntryVersionInt = 0; if (int.TryParse(EntryVersionParts[Index], out EntryVersionInt)) { int NewVersionInt = 0; if (int.TryParse(NewVersionParts[Index], out NewVersionInt)) { if (NewVersionInt < EntryVersionInt) { return; } } else { return; } } else { return; } } Log.TraceInformation("AAR: {0}: {1} newer than {2}", 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; } } //Log.TraceInformation("AAR: {0}", BaseName); AndroidAAREntry AAREntry = new AndroidAAREntry(BaseName, Version, BaseFilename); AARList.Add(AAREntry); // Check for dependencies XDocument DependsXML; string DependencyFilename = BaseFilename + ".pom"; if (File.Exists(DependencyFilename)) { try { DependsXML = XDocument.Load(DependencyFilename); } catch (Exception e) { Log.TraceError("AAR Dependency file {0} parsing error! {1}", DependencyFilename, e); return; } } else { Log.TraceError("AAR: Dependency file {0} 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 = GetElementValue(DependNode, VersionName, ""); string DepScope = GetElementValue(DependNode, ScopeName, "compile"); string DepType = GetElementValue(DependNode, TypeName, "jar"); //Log.TraceInformation("Dependency: {0} {1} {2} {3} {4}", 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); } else if (DepType == "jar") { AddNewJAR(DepGroupId, DepArtifactId, DepVersion); } } } 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); } } /// /// Copies the required JAR files to the provided directory /// /// Destination path for JAR files public void CopyJARs(string DestinationPath) { 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); //Log.TraceInformation("Attempting to copy JAR {0} {1} {2}", Filename, BaseName, TargetPath); if (!File.Exists(Filename)) { Log.TraceInformation("JAR doesn't exist! {0}", Filename); } if (!File.Exists(TargetPath)) { Log.TraceInformation("Copying JAR {0}", BaseName); File.Copy(Filename, TargetPath); } } } /// /// Extracts the required AAR files to the provided directory /// /// Destination path for AAR files /// Name of the package these AARs are being used with public void ExtractAARs(string DestinationPath, string AppPackageName) { MakeDirectoryIfRequired(DestinationPath); DestinationPath = Path.Combine(DestinationPath, "JavaLibs"); MakeDirectoryIfRequired(DestinationPath); Log.TraceInformation("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)) { Log.TraceInformation("Extracting AAR {0}", BaseName); IEnumerable FileNames = UnzipFiles(Entry.Filename + ".aar", TargetPath); // 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) { Log.TraceError("AAR Manifest file {0} parsing error! {1}", 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); } } } } /// /// Extracts the contents of a zip file /// /// Name of the zip file /// Output directory /// List of files written public static IEnumerable UnzipFiles(string ZipFileName, string BaseDirectory) { // 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 OutputFileNames = new List(); 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)); Log.TraceInformation("Changed FileName {0} => {1}", 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; } } } }