// SelfDeclaredAndroidDependencies v3 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using UnityEditor; using UnityEditor.Android; using UnityEngine; namespace Unity.SelfDeclaredAndroidDependencies.Editor { [AttributeUsage(AttributeTargets.Class)] class SelfDeclaredAndroidDependenciesAttribute : Attribute { } [SelfDeclaredAndroidDependencies] abstract class AndroidDependencies : IPostGenerateGradleAndroidProject { const string k_AndroidDependenciesDisableKey = "SelfDeclaredAndroidDependenciesDisabled"; public virtual int callbackOrder { get; } = 1; /// /// The name of the dependant. This is used to identify the section in the build.gradle file. /// public abstract string DependantName { get; } /// /// The list of dependencies to be added to the build.gradle file. /// format: "group:name:version" /// public abstract List Dependencies { get; } /// /// The list of repositories to be added to the settings.gradle file. /// public abstract List Repositories { get; } /// /// The list of properties to be added to the gradle.properties file. /// public abstract List GradleProperties { get; } /// /// The line that marks the start of the dependencies section in the build.gradle file. /// protected string DependenciesLineStart => $"// Dependencies for \"{DependantName}\". This section is automatically generated."; /// /// The line that marks the end of the dependencies section in the build.gradle file. /// protected string DependenciesLineEnd => $"// End of dependencies for \"{DependantName}\"."; /// /// The line that marks the start of the repositories section in the settings.gradle file. /// protected string RepositoriesLineStart => $"// Repositories for \"{DependantName}\". This section is automatically generated."; /// /// The line that marks the end of the repositories section in the settings.gradle file. /// protected string RepositoriesLineEnd => $"// End of repositories for \"{DependantName}\"."; /// /// The line that marks the start of the properties section in the gradle.properties file. /// protected string GradlePropertiesLineStart => $"# Properties for \"{DependantName}\". This section is automatically generated."; /// /// The line that marks the end of the properties section in the gradle.properties file. /// protected string GradlePropertiesLineEnd => $"# End of properties for \"{DependantName}\"."; protected bool IsEnabled { get; set; } = true; void IPostGenerateGradleAndroidProject.OnPostGenerateGradleAndroidProject(string path) { if (string.IsNullOrEmpty(DependantName)) { return; } if (SessionState.GetBool(k_AndroidDependenciesDisableKey, false) || SessionState.GetBool($"{k_AndroidDependenciesDisableKey}:{DependantName}", false)) { IsEnabled = false; } var buildGradle = Path.Combine(path, "build.gradle"); if (GetUnityVersionAsFloat(Application.unityVersion) >= 2022.2f) { var settingsGradle = Path.Combine(path, "..", "settings.gradle"); InjectGradleRepositoriesInSettings(settingsGradle); } else { InjectGradleRepositoriesInUnityLib(buildGradle); } InjectGradleDependencies(buildGradle); var gradleProperties = Path.Combine(path, "..", "gradle.properties"); InjectGradleProperties(gradleProperties); } /// /// Injects the dependencies into the build.gradle file. /// void InjectGradleDependencies(string buildGradle) { var dependencies = GenerateGradleDependencies(); var startLine = DependenciesLineStart; var endLine = DependenciesLineEnd; ReplaceOrAddSectionFile(buildGradle, startLine, endLine, dependencies); } /// /// Injects the repositories into the settings.gradle file. /// /// The path to the settings.gradle file. void InjectGradleRepositoriesInSettings(string settingsGradle) { var repositories = GenerateGradleRepositoriesForSettings(); var startLine = RepositoriesLineStart; var endLine = RepositoriesLineEnd; ReplaceOrAddSectionFile(settingsGradle, startLine, endLine, repositories); } /// /// Injects the repositories into the build.gradle file. /// /// The path to the build.gradle file. void InjectGradleRepositoriesInUnityLib(string buildGradle) { var repositories = GenerateGradleRepositoriesForUnityLib(); var startLine = RepositoriesLineStart; var endLine = RepositoriesLineEnd; ReplaceOrAddSectionFile(buildGradle, startLine, endLine, repositories); } /// /// Injects the properties into the gradle.properties file. /// /// The path to the gradle.properties file. void InjectGradleProperties(string gradleProperties) { var properties = GenerateGradleProperties(); var startLine = GradlePropertiesLineStart; var endLine = GradlePropertiesLineEnd; ReplaceOrAddSectionFile(gradleProperties, startLine, endLine, properties); } void ReplaceOrAddSectionFile(string filename, string sectionStart, string sectionEnd, string sectionNewContent) { var content = File.ReadAllText(filename); content = ReplaceOrAddSectionString(content, sectionStart, sectionEnd, sectionNewContent); File.WriteAllText(filename, content); } string ReplaceOrAddSectionString(string content, string sectionStart, string sectionEnd, string sectionNewContent) { if (content.Contains(sectionStart)) { var startIndex = content.IndexOf(sectionStart, StringComparison.Ordinal); var endIndex = content.IndexOf(sectionEnd, StringComparison.Ordinal) + sectionEnd.Length; var oldSection = content.Substring(startIndex, endIndex - startIndex); var newSection = $"{sectionStart}\n{sectionNewContent}\n{sectionEnd}"; content = content.Replace(oldSection, newSection); } else { content += $"\n{sectionStart}\n{sectionNewContent}\n{sectionEnd}"; } return content; } /// /// Generates the dependencies to be added to the build.gradle file. /// /// A string containing the dependencies to be added to the build.gradle file. protected string GenerateGradleDependencies() { var dependencies = Dependencies; if (dependencies == null || dependencies.Count == 0) { return ""; } var dependencyLines = string.Join("\n", dependencies .Distinct() .Select(dependency => $" implementation '{dependency}'")); var dependencyBlock = $@"afterEvaluate {{ dependencies {{ {dependencyLines} }} }}"; if (!IsEnabled) { dependencyBlock = CommentBlock(dependencyBlock, "//"); } return dependencyBlock; } /// /// Generates the repositories to be added to the settings.gradle file. /// /// A string containing the repositories to be added to the settings.gradle file. protected string GenerateGradleRepositoriesForSettings() { var repositories = Repositories; if (repositories == null || repositories.Count == 0) { return ""; } var repositoriesBlock = string.Join("\n", repositories .Distinct() .Select(repository => $"settings.getDependencyResolutionManagement().getRepositories().maven(mavenRepository -> {{ mavenRepository.setUrl('{repository}'); }})")); if (!IsEnabled) { repositoriesBlock = CommentBlock(repositoriesBlock, "//"); } return repositoriesBlock; } /// /// Generates the repositories to be added to the build.gradle file. /// /// A string containing the repositories to be added to the build.gradle file. protected string GenerateGradleRepositoriesForUnityLib() { var repositories = Repositories; if (repositories == null || repositories.Count == 0) { return ""; } var repositoryLines = string.Join("\n", repositories .Distinct() .Select(repository => $" maven {{ url \"{repository}\" }}")); var repositoryBlock = $@"([rootProject] + (rootProject.subprojects as List)).each {{ project -> project.repositories {{ {repositoryLines} }} }}"; if (!IsEnabled) { repositoryBlock = CommentBlock(repositoryBlock, "//"); } return repositoryBlock; } /// /// Generates the properties to be added to the gradle.properties file. /// /// A string containing the properties to be added to the gradle.properties file. protected string GenerateGradleProperties() { var properties = GradleProperties; if (properties == null || properties.Count == 0) { return ""; } var propertiesBlock = string.Join("\n", properties); if (!IsEnabled) { propertiesBlock = CommentBlock(propertiesBlock, "#"); } return propertiesBlock; } public static string CommentBlock(string block, string commentPrefix) { return string.Join("\n", block.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) .Select(line => $"{commentPrefix} {line.TrimEnd()}")); } /// /// A helper method to extract dependencies from xml files. /// /// The list of xml files to extract dependencies from. /// A list of dependencies and repositories extracted from the xml files. protected static AndroidXmlDependencies DependenciesFromXmlFiles(string[] files) { var dependencies = new AndroidXmlDependencies { Repositories = new List(), Dependencies = new List() }; foreach (var file in files) { var fileDependencies = DependenciesFromXmlFile(file); dependencies.Dependencies.AddRange(fileDependencies.Dependencies); dependencies.Repositories.AddRange(fileDependencies.Repositories); } return dependencies; } /// /// A helper method to extract dependencies from an xml file. /// /// The xml file to extract dependencies from. /// A list of dependencies and repositories extracted from the xml file. protected static AndroidXmlDependencies DependenciesFromXmlFile(string file) { if (!File.Exists(file)) { return null; } var dependencies = new AndroidXmlDependencies { Repositories = new List(), Dependencies = new List() }; XDocument doc = XDocument.Load(file); var androidPackages = doc.Descendants("androidPackage") .Select(package => new { Spec = package.Attribute("spec")?.Value, Repositories = package.Descendants("repository").Select(repo => repo.Value).ToList() }).ToList(); foreach (var androidPackage in androidPackages) { if (androidPackage.Spec != null) { dependencies.Dependencies.Add(androidPackage.Spec); } dependencies.Repositories.AddRange(androidPackage.Repositories); } return dependencies; } public virtual float GetUnityVersionAsFloat(string unityVersion) { if (string.IsNullOrEmpty(unityVersion)) { return 0f; } var versionParts = unityVersion.Split('.'); if (versionParts.Length < 2) { return 0f; } if (!float.TryParse($"{versionParts[0]}.{versionParts[1]}", out var version)) { return 0f; } return version; } public class AndroidXmlDependencies { public List Dependencies { get; set; } public List Repositories { get; set; } } } }