internal static void findDependencies(this PackageDef pkg, List <string> excludeAdd, List <AssemblyData> searchedFiles) { bool foundNew = false; var notFound = new HashSet <string>(); // First update the pre-entered dependencies var installed = new Installation(Directory.GetCurrentDirectory()).GetPackages().Where(p => p.Name != pkg.Name).ToList(); List <string> brokenPackageNames = new List <string>(); List <AssemblyData> getPackageAssemblues(PackageDef pkgDef) { List <AssemblyData> output = new List <AssemblyData>(); foreach (var f in pkgDef.Files) { var asms = searchedFiles.Where(sf => PathUtils.AreEqual(f.FileName, sf.Location)) .Where(sf => IsDotNetAssembly(sf.Location)).ToList(); if (asms.Count == 0 && (Path.GetExtension(f.FileName).Equals(".dll", StringComparison.OrdinalIgnoreCase) || Path.GetExtension(f.FileName).Equals(".exe", StringComparison.OrdinalIgnoreCase))) { if (File.Exists(f.FileName)) { // If the pluginSearcher found assemblies that are located somewhere not expected by the package definition, the package might appear broken. // But if the file found by the pluginSearcher is the same as the one expected by the package definition we should not count it as broken. // This could cause a package to not be added as a dependencies. // E.g. when debugging and the OpenTAP.Cli.dll is both in the root build dir and in "Packages/OpenTAP" var asmsIdenticalFilename = searchedFiles.Where(sf => Path.GetFileName(f.FileName) == Path.GetFileName(sf.Location)); var asmsIdentical = asmsIdenticalFilename.Where(sf => PathUtils.CompareFiles(f.FileName, sf.Location)); output.AddRange(asmsIdentical); continue; } if (!brokenPackageNames.Contains(pkgDef.Name) && IsDotNetAssembly(f.FileName)) { brokenPackageNames.Add(pkgDef.Name); log.Warning($"Package '{pkgDef.Name}' is not installed correctly? Referenced file '{f.FileName}' was not found."); } } output.AddRange(asms); } return(output); }; var packageAssemblies = new Memorizer <PackageDef, List <AssemblyData> >(getPackageAssemblues); // check versions of any hardcoded dependencies against what is currently installed foreach (PackageDependency dep in pkg.Dependencies) { var installedPackage = installed.FirstOrDefault(ip => ip.Name == dep.Name); if (installedPackage != null) { if (dep.Version == null) { dep.Version = new VersionSpecifier(installedPackage.Version, VersionMatchBehavior.Compatible); log.Info("A version was not specified for package dependency {0}. Using installed version ({1}).", dep.Name, dep.Version); } else { if (!dep.Version.IsCompatible(installedPackage.Version)) { throw new ExitCodeException((int)PackageCreateAction.ExitCodes.PackageDependencyError, $"Installed version of {dep.Name} ({installedPackage.Version}) is incompatible with dependency specified in package definition ({dep.Version})."); } } } else { throw new ExitCodeException((int)PackageCreateAction.ExitCodes.PackageDependencyError, $"Package dependency '{dep.Name}' specified in package definition is not installed. Please install a compatible version first."); } } // Find additional dependencies do { foundNew = false; // Find everything we already know about var offeredByDependencies = AssembliesOfferedBy(installed, pkg.Dependencies, false, packageAssemblies).ToList(); var offeredByThis = packageAssemblies[pkg] .Where(f => f != null) .ToList(); var anyOffered = offeredByDependencies.Concat(offeredByThis).ToList(); // Find our dependencies and subtract the above two lists var dependentAssemblyNames = pkg.Files .SelectMany(fs => fs.DependentAssemblies) .Where(r => r.Name != "mscorlib") // Special case. We should not bundle the framework assemblies. .Where(r => !anyOffered.Any(of => AssemblyRefUtils.IsCompatibleReference(of, r))) .Distinct().Where(x => !excludeAdd.Contains(x.Name)).ToList(); // If there's anything left we start resolving if (dependentAssemblyNames.Any()) { // First look in installed packages var packageCandidates = new Dictionary <PackageDef, int>(); foreach (var f in installed) { var candidateAsms = packageAssemblies[f].Where(asm => dependentAssemblyNames.Any(dep => (dep.Name == asm.Name))).ToList(); // Don't consider a package that only matches assemblies in the Dependencies subfolder candidateAsms.RemoveAll(asm => asm.Location.Contains("Dependencies")); // TODO: less lazy check for Dependencies subfolder would be good. if (candidateAsms.Count > 0) { packageCandidates[f] = candidateAsms.Count; } } // Look at the most promising candidate (i.e. the one containing most assemblies with the same names as things we need) PackageDef candidatePkg = packageCandidates.OrderByDescending(k => k.Value).FirstOrDefault().Key; if (candidatePkg != null) { foreach (AssemblyData candidateAsm in packageAssemblies[candidatePkg]) { var requiredAsm = dependentAssemblyNames.FirstOrDefault(dep => dep.Name == candidateAsm.Name); if (requiredAsm != null) { if (OpenTap.Utils.Compatible(candidateAsm.Version, requiredAsm.Version)) { log.Info($"Satisfying assembly reference to {requiredAsm.Name} by adding dependency on package {candidatePkg.Name}"); if (candidateAsm.Version != requiredAsm.Version) { log.Warning($"Version of {requiredAsm.Name} in {candidatePkg.Name} is different from the version referenced in this package ({requiredAsm.Version} vs {candidateAsm.Version})."); log.Warning($"Consider changing your version of {requiredAsm.Name} to {candidateAsm.Version} to match that in {candidatePkg.Name}."); } foundNew = true; } else { var depender = pkg.Files.FirstOrDefault(f => f.DependentAssemblies.Contains(requiredAsm)); if (depender == null) { log.Error($"This package require assembly {requiredAsm.Name} in version {requiredAsm.Version} while that assembly is already installed through package '{candidatePkg.Name}' in version {candidateAsm.Version}."); } else { log.Error($"{Path.GetFileName(depender.FileName)} in this package require assembly {requiredAsm.Name} in version {requiredAsm.Version} while that assembly is already installed through package '{candidatePkg.Name}' in version {candidateAsm.Version}."); } //log.Error($"Please align the version of {requiredAsm.Name} to ensure interoperability with package '{candidate.Key.Name}' or uninstall that package."); throw new ExitCodeException((int)PackageCreateAction.ExitCodes.AssemblyDependencyError, $"Please align the version of {requiredAsm.Name} ({candidateAsm.Version} vs {requiredAsm.Version}) to ensure interoperability with package '{candidatePkg.Name}' or uninstall that package."); } } } if (foundNew) { log.Info("Adding dependency on package '{0}' version {1}", candidatePkg.Name, candidatePkg.Version); PackageDependency pd = new PackageDependency(candidatePkg.Name, new VersionSpecifier(candidatePkg.Version, VersionMatchBehavior.Compatible)); pkg.Dependencies.Add(pd); } } else { // No installed package can offer any of the remaining referenced assemblies. // add them as payload in this package in the Dependencies subfolder foreach (var unknown in dependentAssemblyNames) { var foundAsms = searchedFiles.Where(asm => (asm.Name == unknown.Name) && OpenTap.Utils.Compatible(asm.Version, unknown.Version)).ToList(); var foundAsm = foundAsms.FirstOrDefault(); if (foundAsm != null) { AddFileDependencies(pkg, unknown, foundAsm); packageAssemblies.Invalidate(pkg); foundNew = true; } else if (!notFound.Contains(unknown.Name)) { log.Debug("'{0}' could not be found in any of {1} searched assemblies, or is already added.", unknown.Name, searchedFiles.Count); notFound.Add(unknown.Name); } } } } }while (foundNew); }