#pragma warning disable SA1512,SA1124 // Single-line comments should not be followed by blank line
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using JetBrains.Annotations;
using NugetForUnity.Helper;
using NugetForUnity.PackageSource;
using UnityEngine;
#region No ReShaper
// ReSharper disable All
// needed because 'JetBrains.Annotations.NotNull' and 'System.Diagnostics.CodeAnalysis.NotNull' collide if this file is compiled with a never version of Unity / C#
using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute;
// ReSharper restore All
#endregion
#pragma warning restore SA1512,SA1124 // Single-line comments should not be followed by blank line
namespace NugetForUnity.Configuration
{
///
/// Represents a NuGet.config file that stores the NuGet settings.
/// See here: https://docs.nuget.org/consume/nuget-config-file.
///
public class NugetConfigFile
{
///
/// The file name where the configuration is stored.
///
public const string FileName = "NuGet.config";
///
/// The name of the attribute that is used to configure of
/// .
///
internal const string PackageDownloadUrlTemplateOverwriteAttributeName = "packageDownloadUrlTemplateOverwrite";
///
/// Default timeout in seconds for all web requests.
///
private const int DefaultRequestTimeout = 10;
private const string RequestTimeoutSecondsConfigKey = "RequestTimeoutSeconds";
private const string PackagesConfigDirectoryPathConfigKey = "PackagesConfigDirectoryPath";
private const string ProtocolVersionAttributeName = "protocolVersion";
private const string PasswordAttributeName = "password";
private const string UpdateSearchBatchSizeAttributeName = "updateSearchBatchSize";
private const string SupportsPackageIdSearchFilterAttributeName = "supportsPackageIdSearchFilter";
[NotNull]
private readonly string unityPackagesNugetInstallPath = Path.Combine(UnityPathHelper.AbsoluteUnityPackagesNugetPath, "InstalledPackages");
[NotNull]
private string configuredRepositoryPath = "Packages";
[NotNull]
private string packagesConfigDirectoryPath = Application.dataPath;
[NotNull]
private string repositoryPath = Path.GetFullPath(Path.Combine(Application.dataPath, "Packages"));
///
/// Gets the list of package sources that are defined in the NuGet.config file.
///
///
/// The NuGet server protocol version defaults to version "2" when not pointing to a package source URL ending in .json (e.g.
/// https://api.nuget.org/v3/index.json).
///
[NotNull]
public List PackageSources { get; } = new List();
///
/// Gets the currently active package source that is defined in the NuGet.config file.
/// Note: If the key/Name is set to "All" and the value/Path is set to "(Aggregate source)", all package sources are used.
///
[CanBeNull]
public INugetPackageSource ActivePackageSource { get; private set; }
///
/// Gets the absolute path where packages are to be installed.
///
[NotNull]
public string RepositoryPath
{
get => InstallLocation == PackageInstallLocation.InPackagesFolder ? unityPackagesNugetInstallPath : repositoryPath;
private set => repositoryPath = value;
}
///
/// Gets or sets the incomplete path that is saved. The path is expanded and made public via the property above.
///
[NotNull]
public string ConfiguredRepositoryPath
{
get => configuredRepositoryPath;
set
{
configuredRepositoryPath = value;
var expandedPath = Environment.ExpandEnvironmentVariables(value);
if (!Path.IsPathRooted(expandedPath))
{
expandedPath = Path.Combine(Application.dataPath, expandedPath);
}
RepositoryPath = Path.GetFullPath(expandedPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
}
}
///
/// Gets the default package source to push NuGet packages to.
///
[CanBeNull]
public string DefaultPushSource { get; private set; }
///
/// Gets or sets a value indicating whether to output verbose log messages to the console. False to output the normal level of messages.
///
public bool Verbose { get; set; }
///
/// Gets or sets a value indicating whether to skip installing dependencies and checking for pre-imported Unity libs
/// while auto-restoring.
///
public bool SlimRestore { get; set; } = true;
///
/// Gets or sets a value indicating whether a package is installed from the cache (if present), or if it always downloads the package from the
/// server.
///
public bool InstallFromCache { get; set; }
///
/// Gets or sets a value indicating whether installed package files are set to read-only.
///
public bool ReadOnlyPackageFiles { get; set; }
///
/// Gets or sets absolute path to directory containing packages.config file.
///
[NotNull]
public string PackagesConfigDirectoryPath
{
get =>
InstallLocation == PackageInstallLocation.InPackagesFolder ?
UnityPathHelper.AbsoluteUnityPackagesNugetPath :
packagesConfigDirectoryPath;
set => packagesConfigDirectoryPath = value;
}
///
/// Gets the relative path to directory containing packages.config file. The path is relative to the folder containing the 'NuGet.config' file.
///
[NotNull]
public string RelativePackagesConfigDirectoryPath
{
get => PathHelper.GetRelativePath(Application.dataPath, PackagesConfigDirectoryPath);
private set => PackagesConfigDirectoryPath = Path.GetFullPath(Path.Combine(Application.dataPath, value));
}
///
/// Gets the path to the packages.config file.
///
[NotNull]
public string PackagesConfigFilePath => Path.Combine(PackagesConfigDirectoryPath, PackagesConfigFile.FileName);
///
/// Gets or sets the timeout in seconds used for all web requests to NuGet sources.
///
public int RequestTimeoutSeconds { get; set; } = DefaultRequestTimeout;
///
/// Gets the value that tells the system how to determine where the packages are to be installed and configurations are to be stored.
///
internal PackageInstallLocation InstallLocation { get; private set; }
///
/// Gets the list of enabled plugins.
///
internal List EnabledPlugins { get; } = new List();
///
/// Loads a NuGet.config file at the given file-path.
///
/// The full file-path to the NuGet.config file to load.
/// The newly loaded .
[NotNull]
public static NugetConfigFile Load([NotNull] string filePath)
{
var configFile = new NugetConfigFile
{
InstallFromCache = true, ReadOnlyPackageFiles = false, RelativePackagesConfigDirectoryPath = ".",
};
var file = XDocument.Load(filePath);
// read the full list of package sources (some may be disabled below)
var packageSources = file.Root?.Element("packageSources");
if (packageSources != null)
{
var adds = packageSources.Elements("add");
foreach (var add in adds)
{
var name = add.Attribute("key")?.Value ??
throw new InvalidOperationException($"packageSources misses 'key' attribute. Element:\n{add}");
var path = add.Attribute("value")?.Value ??
throw new InvalidOperationException($"packageSources misses 'value' attribute. Element:\n{add}");
var protocolVersion = add.Attribute(ProtocolVersionAttributeName)?.Value;
var newPackageSource = NugetPackageSourceCreator.CreatePackageSource(name, path, protocolVersion, null);
if (newPackageSource is NugetPackageSourceV3 sourceV3)
{
sourceV3.PackageDownloadUrlTemplateOverwrite = add.Attribute(PackageDownloadUrlTemplateOverwriteAttributeName)?.Value;
var updateSearchBatchSizeString = add.Attribute(UpdateSearchBatchSizeAttributeName)?.Value;
if (!string.IsNullOrEmpty(updateSearchBatchSizeString))
{
sourceV3.UpdateSearchBatchSize = Mathf.Clamp(int.Parse(updateSearchBatchSizeString), 1, int.MaxValue);
}
var supportsPackageIdSearchFilterString = add.Attribute(SupportsPackageIdSearchFilterAttributeName)?.Value;
if (bool.TryParse(supportsPackageIdSearchFilterString, out var supportsPackageIdSearchFilter))
{
sourceV3.SupportsPackageIdSearchFilter = supportsPackageIdSearchFilter;
}
}
configFile.PackageSources.Add(newPackageSource);
}
}
// read the active package source (may be an aggregate of all enabled sources!)
var activePackageSource = file.Root?.Element("activePackageSource");
if (activePackageSource != null)
{
var add = activePackageSource.Element("add") ??
throw new InvalidOperationException($"activePackageSource misses 'add' element. Element:\n{activePackageSource}");
var name = add.Attribute("key")?.Value ??
throw new InvalidOperationException($"activePackageSource misses 'key' attribute. Element:\n{add}");
var path = add.Attribute("value")?.Value ??
throw new InvalidOperationException($"activePackageSource misses 'value' attribute. Element:\n{add}");
configFile.ActivePackageSource = NugetPackageSourceCreator.CreatePackageSource(name, path, null, configFile.PackageSources);
}
// disable all listed disabled package sources
var disabledPackageSources = file.Root?.Element("disabledPackageSources");
if (disabledPackageSources != null)
{
var adds = disabledPackageSources.Elements("add");
foreach (var add in adds)
{
var name = add.Attribute("key")?.Value;
var disabled = add.Attribute("value")?.Value;
if (string.Equals(disabled, "true", StringComparison.OrdinalIgnoreCase))
{
var source = configFile.PackageSources.Find(p => p.Name == name);
if (source != null)
{
source.IsEnabled = false;
}
}
}
}
// read the list of enabled plugins
var enabledPlugins = file.Root?.Element("enabledPlugins");
if (enabledPlugins != null)
{
foreach (var add in enabledPlugins.Elements("add"))
{
var name = add.Attribute("name")?.Value;
var path = add.Attribute("path")?.Value;
if (name != null && path != null)
{
configFile.EnabledPlugins.Add(new NugetForUnityPluginId(name, path));
}
}
}
// set all listed passwords for package source credentials
var packageSourceCredentials = file.Root?.Element("packageSourceCredentials");
if (packageSourceCredentials != null)
{
foreach (var sourceElement in packageSourceCredentials.Elements())
{
var name = XmlConvert.DecodeName(sourceElement.Name.LocalName);
var source = configFile.PackageSources.Find(p => p.Name == name);
if (source == null)
{
continue;
}
var adds = sourceElement.Elements("add");
foreach (var add in adds)
{
var keyName = add.Attribute("key")?.Value;
if (string.Equals(keyName, "userName", StringComparison.OrdinalIgnoreCase))
{
var userName = add.Attribute("value")?.Value;
source.UserName = userName;
}
else if (string.Equals(keyName, "clearTextPassword", StringComparison.OrdinalIgnoreCase))
{
var password = add.Attribute("value")?.Value;
source.SavedPassword = password;
source.SavedPasswordIsEncrypted = false;
}
else if (string.Equals(keyName, PasswordAttributeName, StringComparison.OrdinalIgnoreCase))
{
var encryptedPassword = add.Attribute("value")?.Value;
source.SavedPassword = encryptedPassword;
source.SavedPasswordIsEncrypted = true;
}
}
}
}
// read the configuration data
var config = file.Root?.Element("config");
if (config == null)
{
return configFile;
}
var addElements = config.Elements("add");
foreach (var add in addElements)
{
var key = add.Attribute("key")?.Value;
var value = add.Attribute("value")?.Value ?? throw new InvalidOperationException($"config misses 'value' attribute. Element:\n{add}");
if (string.Equals(key, "packageInstallLocation", StringComparison.OrdinalIgnoreCase))
{
configFile.InstallLocation = (PackageInstallLocation)Enum.Parse(typeof(PackageInstallLocation), value);
}
else if (string.Equals(key, "repositoryPath", StringComparison.OrdinalIgnoreCase))
{
configFile.ConfiguredRepositoryPath = value;
}
else if (string.Equals(key, "DefaultPushSource", StringComparison.OrdinalIgnoreCase))
{
configFile.DefaultPushSource = value;
}
else if (string.Equals(key, "verbose", StringComparison.OrdinalIgnoreCase))
{
configFile.Verbose = bool.Parse(value);
}
else if (string.Equals(key, "slimRestore", StringComparison.OrdinalIgnoreCase))
{
configFile.SlimRestore = bool.Parse(value);
}
else if (string.Equals(key, "InstallFromCache", StringComparison.OrdinalIgnoreCase))
{
configFile.InstallFromCache = bool.Parse(value);
}
else if (string.Equals(key, "ReadOnlyPackageFiles", StringComparison.OrdinalIgnoreCase))
{
configFile.ReadOnlyPackageFiles = bool.Parse(value);
}
else if (string.Equals(key, RequestTimeoutSecondsConfigKey, StringComparison.OrdinalIgnoreCase))
{
configFile.RequestTimeoutSeconds = int.Parse(value, CultureInfo.InvariantCulture);
}
else if (string.Equals(key, PackagesConfigDirectoryPathConfigKey, StringComparison.OrdinalIgnoreCase))
{
configFile.RelativePackagesConfigDirectoryPath = value;
}
}
return configFile;
}
///
/// Creates a NuGet.config file with the default settings at the given full file-path.
///
/// The full file-path where to create the NuGet.config file.
/// The loaded loaded off of the newly created default file.
[NotNull]
public static NugetConfigFile CreateDefaultFile([NotNull] string filePath)
{
const string contents = @"
";
File.WriteAllText(filePath, contents, new UTF8Encoding());
return Load(filePath);
}
///
/// Saves this NuGet.config file to disk.
///
/// The file-path to where this NuGet.config will be saved.
[SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "We intentionally use lower case.")]
public void Save([NotNull] string filePath)
{
var configFile = new XDocument();
var packageSources = new XElement("packageSources");
var disabledPackageSources = new XElement("disabledPackageSources");
var packageSourceCredentials = new XElement("packageSourceCredentials");
XElement addElement;
// save all enabled and disabled package sources
foreach (var source in PackageSources)
{
addElement = new XElement("add");
addElement.Add(new XAttribute("key", source.Name));
addElement.Add(new XAttribute("value", source.SavedPath));
if (!string.IsNullOrEmpty(source.SavedProtocolVersion))
{
addElement.Add(new XAttribute(ProtocolVersionAttributeName, source.SavedProtocolVersion));
}
if (source is NugetPackageSourceV3 sourceV3)
{
if (!string.IsNullOrEmpty(sourceV3.PackageDownloadUrlTemplateOverwrite))
{
addElement.Add(
new XAttribute(PackageDownloadUrlTemplateOverwriteAttributeName, sourceV3.PackageDownloadUrlTemplateOverwrite));
}
if (sourceV3.UpdateSearchBatchSize != NugetPackageSourceV3.DefaultUpdateSearchBatchSize)
{
addElement.Add(new XAttribute(UpdateSearchBatchSizeAttributeName, sourceV3.UpdateSearchBatchSize));
}
if (sourceV3.SupportsPackageIdSearchFilter != NugetPackageSourceV3.DefaultSupportsPackageIdSearchFilter)
{
addElement.Add(new XAttribute(SupportsPackageIdSearchFilterAttributeName, sourceV3.SupportsPackageIdSearchFilter));
}
}
packageSources.Add(addElement);
if (!source.IsEnabled)
{
addElement = new XElement("add");
addElement.Add(new XAttribute("key", source.Name));
addElement.Add(new XAttribute("value", "true"));
disabledPackageSources.Add(addElement);
}
if (source.HasPassword)
{
var sourceElement = new XElement(XmlConvert.EncodeName(source.Name) ?? string.Empty);
packageSourceCredentials.Add(sourceElement);
addElement = new XElement("add");
addElement.Add(new XAttribute("key", "userName"));
addElement.Add(new XAttribute("value", source.UserName ?? string.Empty));
sourceElement.Add(addElement);
addElement = new XElement("add");
addElement.Add(new XAttribute("key", source.SavedPasswordIsEncrypted ? PasswordAttributeName : "clearTextPassword"));
addElement.Add(new XAttribute("value", source.SavedPassword ?? string.Empty));
sourceElement.Add(addElement);
}
}
// save the active package source (may be an aggregate)
var activePackageSource = new XElement("activePackageSource");
addElement = new XElement("add");
addElement.Add(new XAttribute("key", "All"));
addElement.Add(new XAttribute("value", "(Aggregate source)"));
activePackageSource.Add(addElement);
var config = new XElement("config");
addElement = new XElement("add");
addElement.Add(new XAttribute("key", "packageInstallLocation"));
addElement.Add(new XAttribute("value", InstallLocation.ToString()));
config.Add(addElement);
if (!string.IsNullOrEmpty(ConfiguredRepositoryPath))
{
// save the un-expanded repository path
addElement = new XElement("add");
addElement.Add(new XAttribute("key", "repositoryPath"));
addElement.Add(new XAttribute("value", ConfiguredRepositoryPath));
config.Add(addElement);
}
addElement = new XElement("add");
addElement.Add(new XAttribute("key", PackagesConfigDirectoryPathConfigKey));
addElement.Add(new XAttribute("value", PathHelper.GetRelativePath(Application.dataPath, packagesConfigDirectoryPath)));
config.Add(addElement);
// save the default push source
if (DefaultPushSource != null)
{
addElement = new XElement("add");
addElement.Add(new XAttribute("key", "DefaultPushSource"));
addElement.Add(new XAttribute("value", DefaultPushSource));
config.Add(addElement);
}
if (Verbose)
{
addElement = new XElement("add");
addElement.Add(new XAttribute("key", "verbose"));
addElement.Add(new XAttribute("value", Verbose.ToString().ToLowerInvariant()));
config.Add(addElement);
}
addElement = new XElement("add");
addElement.Add(new XAttribute("key", "slimRestore"));
addElement.Add(new XAttribute("value", SlimRestore.ToString().ToLowerInvariant()));
config.Add(addElement);
if (!InstallFromCache)
{
addElement = new XElement("add");
addElement.Add(new XAttribute("key", "InstallFromCache"));
addElement.Add(new XAttribute("value", InstallFromCache.ToString().ToLowerInvariant()));
config.Add(addElement);
}
if (!ReadOnlyPackageFiles)
{
addElement = new XElement("add");
addElement.Add(new XAttribute("key", "ReadOnlyPackageFiles"));
addElement.Add(new XAttribute("value", ReadOnlyPackageFiles.ToString().ToLowerInvariant()));
config.Add(addElement);
}
if (RequestTimeoutSeconds != DefaultRequestTimeout)
{
addElement = new XElement("add");
addElement.Add(new XAttribute("key", RequestTimeoutSecondsConfigKey));
addElement.Add(new XAttribute("value", RequestTimeoutSeconds));
config.Add(addElement);
}
var configuration = new XElement("configuration");
configuration.Add(packageSources);
configuration.Add(disabledPackageSources);
configuration.Add(packageSourceCredentials);
configuration.Add(activePackageSource);
if (EnabledPlugins.Count > 0)
{
var enabledPlugins = new XElement("enabledPlugins");
foreach (var plugin in EnabledPlugins)
{
addElement = new XElement("add");
addElement.Add(new XAttribute("name", plugin.Name));
addElement.Add(new XAttribute("path", plugin.Path));
enabledPlugins.Add(addElement);
}
configuration.Add(enabledPlugins);
}
configuration.Add(config);
configFile.Add(configuration);
var fileExists = File.Exists(filePath);
// remove the read only flag on the file, if there is one.
if (fileExists)
{
var attributes = File.GetAttributes(filePath);
if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
attributes &= ~FileAttributes.ReadOnly;
File.SetAttributes(filePath, attributes);
}
}
configFile.Save(filePath);
}
///
/// Changes the package install location config and also moves the packages.config to the new location.
///
/// New install location to set.
internal void ChangeInstallLocation(PackageInstallLocation newInstallLocation)
{
if (newInstallLocation == InstallLocation)
{
return;
}
var oldPackagesConfigPath = PackagesConfigFilePath;
InstallLocation = newInstallLocation;
UnityPathHelper.EnsurePackageInstallDirectoryIsSetup();
var newConfigPath = PackagesConfigFilePath;
File.Move(oldPackagesConfigPath, newConfigPath);
var configMeta = oldPackagesConfigPath + ".meta";
if (File.Exists(configMeta))
{
File.Move(configMeta, newConfigPath + ".meta");
}
}
}
}