using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using JetBrains.Annotations; using NugetForUnity.Models; using UnityEditor; using UnityEngine; namespace NugetForUnity.Configuration { /// /// Represents a package.config file that holds the NuGet package dependencies for the project. /// public class PackagesConfigFile { /// /// The file name where the configuration is stored. /// public const string FileName = "packages.config"; private const string AutoReferencedAttributeName = "autoReferenced"; private const string TargetFrameworkAttributeName = "targetFramework"; [CanBeNull] private string contentIsSameAsInFilePath; /// /// Gets the s contained in the package.config file. /// [NotNull] public List Packages { get; private set; } = new List(); /// /// Loads a list of all currently installed packages by reading the packages.config file. /// /// A newly created . [NotNull] public static PackagesConfigFile Load() { var filePath = ConfigurationManager.NugetConfigFile.PackagesConfigFilePath; var configFile = new PackagesConfigFile { Packages = new List() }; // Create a package.config file, if there isn't already one in the project if (!File.Exists(filePath)) { Debug.LogFormat("No packages.config file found. Creating default at {0}", filePath); configFile.Save(); } var packagesFile = XDocument.Load(filePath); if (packagesFile.Root is null) { throw new InvalidOperationException("XML has no root element."); } foreach (var packageElement in packagesFile.Root.Elements()) { var package = new PackageConfig { Id = packageElement.Attribute("id")?.Value ?? throw new InvalidOperationException($"package element misses 'id' attribute. Element:\n{packageElement}"), Version = packageElement.Attribute("version")?.Value ?? throw new InvalidOperationException($"package element misses 'version' attribute. Element:\n{packageElement}"), IsManuallyInstalled = packageElement.Attribute("manuallyInstalled")?.Value.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false, AutoReferenced = (bool)(packageElement.Attributes(AutoReferencedAttributeName).FirstOrDefault() ?? new XAttribute(AutoReferencedAttributeName, true)), TargetFramework = packageElement.Attribute(TargetFrameworkAttributeName)?.Value, }; configFile.Packages.Add(package); } configFile.contentIsSameAsInFilePath = filePath; return configFile; } /// /// Adds a package to the packages.config file. /// /// The NugetPackage to add to the packages.config file. /// The newly added or allready existing config entry from the packages.config file. public PackageConfig AddPackage([NotNull] INugetPackageIdentifier package) { return AddPackage(new PackageConfig { Id = package.Id, Version = package.Version, IsManuallyInstalled = package.IsManuallyInstalled }); } /// /// Removes a package from the packages.config file. /// /// The NugetPackage to remove from the packages.config file. /// True if the package was removed, false otherwise. public bool RemovePackage([NotNull] INugetPackageIdentifier package) { var removed = Packages.RemoveAll(p => p.Equals(package)); if (removed > 0) { MarkAsModified(); } return removed > 0; } /// /// Saves the packages.config file and populates it with given installed NugetPackages. /// public void Save() { var filePath = ConfigurationManager.NugetConfigFile.PackagesConfigFilePath; if (contentIsSameAsInFilePath == filePath) { return; } Packages.Sort( (x, y) => { if (x.Id == y.Id) { return x.PackageVersion.CompareTo(y.PackageVersion); } return string.Compare(x.Id, y.Id, StringComparison.Ordinal); }); var packagesFile = new XDocument(); packagesFile.Add(new XElement("packages")); foreach (var package in Packages) { var packageElement = new XElement("package"); packageElement.Add(new XAttribute("id", package.Id)); packageElement.Add(new XAttribute("version", package.Version)); if (package.IsManuallyInstalled) { packageElement.Add(new XAttribute("manuallyInstalled", "true")); } if (!package.AutoReferenced) { packageElement.Add(new XAttribute(AutoReferencedAttributeName, package.AutoReferenced)); } if (!string.IsNullOrEmpty(package.TargetFramework)) { packageElement.Add(new XAttribute(TargetFrameworkAttributeName, package.TargetFramework)); } packagesFile.Root?.Add(packageElement); } // remove the read only flag on the file, if there is one. var packageExists = File.Exists(filePath); if (packageExists) { var attributes = File.GetAttributes(filePath); if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) { attributes &= ~FileAttributes.ReadOnly; File.SetAttributes(filePath, attributes); } } packagesFile.Save(filePath); contentIsSameAsInFilePath = filePath; } /// /// Moves the packages.config file and its corresponding meta file to the given path. If there is no packages.config file on the current /// path, makes a default version of the file on the new path. /// /// Path to move packages.config file to. internal static void Move([NotNull] string newPath) { var nugetConfig = ConfigurationManager.NugetConfigFile; var oldFilePath = nugetConfig.PackagesConfigFilePath; var oldPath = nugetConfig.PackagesConfigDirectoryPath; // We need to make sure saved path is using forward slashes so it works on all systems nugetConfig.PackagesConfigDirectoryPath = newPath.Replace("\\", "/"); var newFilePath = Path.GetFullPath(Path.Combine(newPath, FileName)); try { if (!File.Exists(oldFilePath)) { Debug.LogFormat("No packages.config file found. Creating default at {0}", newPath); var configFile = new PackagesConfigFile { Packages = new List() }; configFile.Save(); AssetDatabase.Refresh(); return; } Directory.CreateDirectory(newPath); // moving config to the new path File.Move(oldFilePath, newFilePath); } catch (Exception e) { // usually unauthorized access or IO exception (trying to move to a folder where the same file exists) Debug.LogException(e); nugetConfig.PackagesConfigDirectoryPath = oldPath; return; } // manually moving meta file to suppress Unity warning if (File.Exists($"{oldFilePath}.meta")) { File.Move($"{oldFilePath}.meta", $"{newFilePath}.meta"); } // if the old path is now an empty directory, delete it if (!Directory.EnumerateFileSystemEntries(oldPath).Any()) { Directory.Delete(oldPath); // also delete its meta file if it exists if (File.Exists($"{oldPath}.meta")) { File.Delete($"{oldPath}.meta"); } } } /// /// Sets the manually installed flag for the given package. /// /// The package to mark as manually installed. internal void SetManuallyInstalledFlag([NotNull] INugetPackageIdentifier package) { package.IsManuallyInstalled = true; var packageConfig = GetPackageConfigurationById(package); if (packageConfig != null && !packageConfig.IsManuallyInstalled) { packageConfig.IsManuallyInstalled = true; MarkAsModified(); } } /// /// Gets the entry of the NuGet package with the id . /// /// The package id to serch. /// The configuration of the NuGet package or null if the package is not in the collection. [CanBeNull] internal PackageConfig GetPackageConfigurationById([NotNull] INugetPackageIdentifier package) { return GetPackageConfigurationById(package.Id); } /// /// Gets the entry of the NuGet package with the id . /// /// The package id to serch. /// The configuration of the NuGet package or null if the package is not in the collection. [CanBeNull] internal PackageConfig GetPackageConfigurationById([NotNull] string packageId) { return Packages.Find(p => string.Equals(p.Id, packageId, StringComparison.OrdinalIgnoreCase)); } /// /// Adds a package to the packages.config file. /// /// The NugetPackage to add to the packages.config file. /// The newly added or allready existing config entry from the packages.config file. private PackageConfig AddPackage([NotNull] PackageConfig package) { var existingPackage = GetPackageConfigurationById(package); if (existingPackage is null) { Packages.Add(package); MarkAsModified(); return package; } var compared = existingPackage.CompareTo(package); if (compared < 0) { Debug.LogWarningFormat( "{0} {1} is already listed in the packages.config file. Updating to {2}", existingPackage.Id, existingPackage.Version, package.Version); Packages.Remove(existingPackage); Packages.Add(package); MarkAsModified(); return package; } if (compared > 0) { Debug.LogWarningFormat( "Trying to add {0} {1} to the packages.config file. {2} is already listed, so using that.", package.Id, package.Version, existingPackage.Version); return existingPackage; } return existingPackage; } private void MarkAsModified() { contentIsSameAsInFilePath = null; } } }