/// <summary> /// Determine whether the current set of artifacts in the project matches the set of /// specified dependencies. /// </summary> /// <param name="destDirectory">Directory where dependencies are located in the /// project. If this parameter is null, a dictionary of required dependencies is /// always returned.</param> /// <param name="explodeAar">Delegate that determines whether a dependency should be /// exploded. If a dependency is currently exploded but shouldn't be according to this /// delegate, the dependency is deleted.</param> /// <returns>null if all dependencies are present, dictionary of all required dependencies /// otherwise.</returns> public Dictionary <string, Dependency> DependenciesPresent(string destDirectory, ExplodeAar explodeAar = null) { Dictionary <string, Dependency> dependencyMap = LoadDependencies(true, keepMissing: true, findCandidates: true); // If a destination directory was specified, determine whether the dependencies // referenced by dependencyMap differ to what is present in the project. If they // are the same, we can skip this entire method. if (destDirectory != null) { if (DependenciesEqual(GetCurrentDependencies(dependencyMap, destDirectory, explodeAar: explodeAar, repoPaths: repositoryPaths).Keys, dependencyMap.Keys)) { Log("All dependencies up to date.", verbose: true); return(null); } } return(dependencyMap); }
/// <summary> /// Performs the resolution process. This determines the versions of the /// dependencies that should be used. Transitive dependencies are also /// processed. /// </summary> /// <returns>The dependencies. The key is the "versionless" key of the dependency. /// </returns> /// <param name="useLatest">If set to <c>true</c> use latest version of a conflicting /// dependency. /// if <c>false</c> a ResolutionException is thrown in the case of a conflict.</param> /// <param name="destDirectory">Directory dependencies will be copied to using /// CopyDependencies().</param> /// <param name="explodeAar">Delegate that determines whether a dependency should be /// exploded. If a dependency is currently exploded but shouldn't be according to this /// delegate, the dependency is deleted.</param> public Dictionary <string, Dependency> ResolveDependencies( bool useLatest, string destDirectory = null, ExplodeAar explodeAar = null) { List <Dependency> unresolved = new List <Dependency>(); Dictionary <string, Dependency> candidates = new Dictionary <string, Dependency>(); Dictionary <string, Dependency> dependencyMap = DependenciesPresent(destDirectory, explodeAar: explodeAar); if (dependencyMap == null) { return(candidates); } // Set of each versioned dependencies for each version-less dependency key. // e.g if foo depends upon bar, map[bar] = {foo}. var reverseDependencyTree = new Dictionary <string, HashSet <string> >(); var warnings = new HashSet <string>(); // All dependencies are added to the "unresolved" list. foreach (var dependency in dependencyMap.Values) { unresolved.Add(FindCandidate(dependency)); } do { Dictionary <string, Dependency> nextUnresolved = new Dictionary <string, Dependency>(); foreach (Dependency dep in unresolved) { var currentDep = dep; // Whether the dependency has been resolved and therefore should be removed // from the unresolved list. bool removeDep = true; // check for existing candidate Dependency candidate; Dependency newCandidate; if (candidates.TryGetValue(currentDep.VersionlessKey, out candidate)) { if (currentDep.IsAcceptableVersion(candidate.BestVersion)) { removeDep = true; // save the most restrictive dependency in the // candidate if (currentDep.IsNewer(candidate)) { candidates[currentDep.VersionlessKey] = currentDep; } } else { // in general, we need to iterate removeDep = false; // refine one or both dependencies if they are // non-concrete. bool possible = false; if (currentDep.Version.Contains("+") && candidate.IsNewer(currentDep)) { possible = currentDep.RefineVersionRange(candidate); } // only try if the candidate is less than the depenceny if (candidate.Version.Contains("+") && currentDep.IsNewer(candidate)) { possible = possible || candidate.RefineVersionRange(currentDep); } if (possible) { // add all the dependency constraints back to make // sure all are met. foreach (Dependency d in dependencyMap.Values) { if (d.VersionlessKey == candidate.VersionlessKey) { if (!nextUnresolved.ContainsKey(d.Key)) { nextUnresolved.Add(d.Key, d); } } } } else if (!possible && useLatest) { // Reload versions of the dependency has they all have been // removed. newCandidate = (currentDep.IsNewer(candidate) ? currentDep : candidate); newCandidate = newCandidate.HasPossibleVersions ? newCandidate : FindCandidate(newCandidate); candidates[newCandidate.VersionlessKey] = newCandidate; currentDep = newCandidate; removeDep = true; // Due to a dependency being included via multiple modules we track // whether a warning has already been reported and make sure it's // only reported once. if (!warnings.Contains(currentDep.VersionlessKey)) { // If no parents of this dependency are found the app // must have specified the dependency. string requiredByString = currentDep.VersionlessKey + " required by (this app)"; // Print dependencies to aid debugging. var dependenciesMessage = new List <string>(); dependenciesMessage.Add("Found dependencies:"); dependenciesMessage.Add(requiredByString); foreach (var kv in reverseDependencyTree) { string requiredByMessage = String.Format( "{0} required by ({1})", kv.Key, String.Join( ", ", (new List <string>(kv.Value)).ToArray())); dependenciesMessage.Add(requiredByMessage); if (kv.Key == currentDep.VersionlessKey) { requiredByString = requiredByMessage; } } Log(String.Join("\n", dependenciesMessage.ToArray())); Log(String.Format( "WARNING: No compatible versions of {0}, will try using " + "the latest version {1}", requiredByString, currentDep.BestVersion)); warnings.Add(currentDep.VersionlessKey); } } else if (!possible) { throw new ResolutionException("Cannot resolve " + currentDep + " and " + candidate); } } } else { candidate = FindCandidate(currentDep); if (candidate != null) { candidates.Add(candidate.VersionlessKey, candidate); removeDep = true; } else { throw new ResolutionException("Cannot resolve " + currentDep); } } // If the dependency has been found. if (removeDep) { // Add all transitive dependencies to resolution list. foreach (Dependency d in GetDependencies(currentDep)) { if (!nextUnresolved.ContainsKey(d.Key)) { Log("For " + currentDep.Key + " adding dep " + d.Key, verbose: true); HashSet <string> parentNames; if (!reverseDependencyTree.TryGetValue(d.VersionlessKey, out parentNames)) { parentNames = new HashSet <string>(); } parentNames.Add(currentDep.Key); reverseDependencyTree[d.VersionlessKey] = parentNames; nextUnresolved.Add(d.Key, d); } } } else { if (!nextUnresolved.ContainsKey(currentDep.Key)) { nextUnresolved.Add(currentDep.Key, currentDep); } } } unresolved.Clear(); unresolved.AddRange(nextUnresolved.Values); nextUnresolved.Clear(); }while (unresolved.Count > 0); return(candidates); }
/// <summary> /// Get the current set of dependencies referenced by the project. /// </summary> /// <param name="dependencies">Dependencies to search for in the specified /// directory.</param> /// <param name="destDirectory">Directory where dependencies are located in the /// project.</param> /// <param name="explodeAar">Delegate that determines whether a dependency should be /// exploded. If a dependency is currently exploded but shouldn't be according to this /// delegate, the dependency is deleted.</param> /// <param name="repoPaths">Set of additional repo paths to search for the /// dependencies.</param> /// <returns>Dictionary indexed by Dependency.Key where each item is a tuple of /// Dependency instance and the path to the dependency in destDirectory.</returns> public static Dictionary <string, KeyValuePair <Dependency, string> > GetCurrentDependencies( Dictionary <string, Dependency> dependencies, string destDirectory, ExplodeAar explodeAar = null, List <string> repoPaths = null) { var currentDependencies = new Dictionary <string, KeyValuePair <Dependency, string> >(); if (dependencies.Count == 0) { return(currentDependencies); } // Copy the set of dependencies. var transitiveDependencies = new Dictionary <string, Dependency>(dependencies); // Expand set of transitive dependencies into the dictionary of dependencies. foreach (var rootDependency in dependencies.Values) { foreach (var transitiveDependency in GetDependencies(rootDependency, repoPaths)) { transitiveDependencies[transitiveDependency.Key] = transitiveDependency; } } // TODO(smiles): Need a callback that queries Unity's asset DB rather than touching // the filesystem here. string[] filesInDestDir = Directory.GetFileSystemEntries(destDirectory); foreach (var path in filesInDestDir) { // Ignore Unity's .meta files. if (path.EndsWith(MetaExtension)) { continue; } string filename = Path.GetFileName(path); // Strip the package extension from filenames. Directories generated from // unpacked AARs do not have extensions. bool pathIsDirectory = Directory.Exists(path); string filenameWithoutExtension = pathIsDirectory ? filename : Path.GetFileNameWithoutExtension(filename); foreach (var dep in transitiveDependencies.Values) { // Get the set of artifacts matching artifact-*. // The "-" is important to distinguish art-1.0.0 from artifact-1.0.0 // (or base- and basement-). var match = System.Text.RegularExpressions.Regex.Match( filenameWithoutExtension, String.Format("^{0}-.*", dep.Artifact)); if (match.Success) { // Extract the version from the filename. // dep.Artifact is the name of the package (prefix) // The regular expression extracts the version number from the filename // handling filenames like foo-1.2.3-alpha. match = System.Text.RegularExpressions.Regex.Match( filenameWithoutExtension.Substring( dep.Artifact.Length + 1), "^([0-9.]+)"); if (match.Success) { bool reportDependency = true; // If the AAR is exploded and it should not be, delete it and do not // report this dependency. if (pathIsDirectory && explodeAar != null) { string aarFile = dep.BestVersionArtifact; if (aarFile != null && !explodeAar(aarFile)) { DeleteExistingFileOrDirectory(path, includeMetaFiles: true); reportDependency = false; } } if (reportDependency) { string artifactVersion = match.Groups[1].Value; Dependency currentDep = new Dependency( dep.Group, dep.Artifact, artifactVersion, packageIds: dep.PackageIds, repositories: dep.Repositories); // Add the artifact version so BestVersion == Version. currentDep.AddVersion(currentDep.Version); currentDependencies[currentDep.Key] = new KeyValuePair <Dependency, string>(currentDep, path); } break; } } } } return(currentDependencies); }