Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,46 @@ private XmlDocument GenerateDocument(IEnumerable<NuGetPackageInfo> packages)
propertyGroup.AppendChild(managePackageVersionsCentrally);
project.AppendChild(propertyGroup);

var itemGroup = doc.CreateElement(string.Empty, "ItemGroup", string.Empty);
var sortedPackages = packages.OrderBy(x => x.Id, StringComparer.InvariantCultureIgnoreCase).ToList();

foreach (var package in packages)
var unconditionalGroup = doc.CreateElement(string.Empty, "ItemGroup", string.Empty);

var conditionalGroups = sortedPackages
.Where(p => p.Condition is not null)
.GroupBy(p => p.Condition!)
.ToList();

foreach (var package in sortedPackages.Where(p => p.Condition is null))
{
var packageVersion = doc.CreateElement(string.Empty, "PackageVersion", string.Empty);
packageVersion.SetAttribute("Include", package.Id);
packageVersion.SetAttribute("Version", package.Version);
AddPackageVersion(doc, unconditionalGroup, package);
}

itemGroup.AppendChild(packageVersion);
if (unconditionalGroup.HasChildNodes)
{
project.AppendChild(unconditionalGroup);
}

project.AppendChild(itemGroup);
foreach (var group in conditionalGroups)
{
var itemGroup = doc.CreateElement(string.Empty, "ItemGroup", string.Empty);
itemGroup.SetAttribute("Condition", group.Key);

foreach (var package in group.OrderBy(p => p.Id, StringComparer.InvariantCultureIgnoreCase))
{
AddPackageVersion(doc, itemGroup, package);
}

project.AppendChild(itemGroup);
}

return doc;
}

private static void AddPackageVersion(XmlDocument doc, XmlElement itemGroup, NuGetPackageInfo package)
{
var packageVersion = doc.CreateElement(string.Empty, "PackageVersion", string.Empty);
packageVersion.SetAttribute("Include", package.Id);
packageVersion.SetAttribute("Version", package.Version);
itemGroup.AppendChild(packageVersion);
}
}
202 changes: 164 additions & 38 deletions src/CentralPackageManagementMigrator/Builders/ProjectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public ReadOnlyDictionary<string, List<NuGetPackageInfo>> GetPackagesInAllProjec

var packagesInProject = GetPackagesInProject(projectDocument);

packagesInProject = ApplyTargetFrameworkCondition(packagesInProject, GetProjectTargetFrameworks(projectDocument));

if (packagesInProject.Count > 0)
{
allPackages.Add(projectFile, packagesInProject);
Expand All @@ -67,14 +69,136 @@ public ReadOnlyDictionary<string, List<NuGetPackageInfo>> GetPackagesInAllProjec
return allPackages.AsReadOnly();
}

/// <summary>
/// For unit tests.
/// </summary>
internal HashSet<string> GetTargetFrameworksFromSource(string projectSource)
{
var doc = CreateXmlDocument();
doc.LoadXml(projectSource);
var frameworks = new HashSet<string>();

var single = doc.SelectSingleNode("//TargetFramework");
if (single is not null)
{
frameworks.Add(single.InnerText.Trim());
}

var multiple = doc.SelectSingleNode("//TargetFrameworks");
if (multiple is not null)
{
foreach (var tf in multiple.InnerText.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
frameworks.Add(tf);
}
}

return frameworks;
}

public HashSet<string> GetTargetFrameworks(string searchPath)
{
const string searchPattern = "*.csproj";

_logger.LogInformation("Collecting target frameworks from {SearchPattern} files under: {SearchPath}",
searchPattern, searchPath);
var allProjects = Directory.GetFiles(searchPath, searchPattern, SearchOption.AllDirectories);

var frameworks = new HashSet<string>();

foreach (var projectFile in allProjects)
{
var doc = CreateXmlDocument();
doc.Load(projectFile);

var single = doc.SelectSingleNode("//TargetFramework");
if (single is not null)
{
frameworks.Add(single.InnerText.Trim());
continue;
}

var multiple = doc.SelectSingleNode("//TargetFrameworks");
if (multiple is not null)
{
foreach (var tf in multiple.InnerText.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
frameworks.Add(tf);
}
}
}

return frameworks;
}

private static HashSet<string> GetProjectTargetFrameworks(XmlDocument doc)
{
var frameworks = new HashSet<string>();

var single = doc.SelectSingleNode("//TargetFramework");
if (single is not null)
{
frameworks.Add(single.InnerText.Trim());
return frameworks;
}

var multiple = doc.SelectSingleNode("//TargetFrameworks");
if (multiple is not null)
{
foreach (var tf in multiple.InnerText.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
frameworks.Add(tf);
}
}

return frameworks;
}

private static List<NuGetPackageInfo> ApplyTargetFrameworkCondition(List<NuGetPackageInfo> packages, HashSet<string> projectTfs)
{
if (projectTfs.Count == 0)
{
return packages;
}

if (projectTfs.Count == 1)
{
var singleTf = projectTfs.Single();
var tfCondition = $"'$(TargetFramework)' == '{singleTf}'";
return packages.Select(pkg => pkg.Condition is null
? pkg.WithCondition(tfCondition)
: pkg.WithCondition($"{pkg.Condition} and {tfCondition}"))
.ToList();
}

var result = new List<NuGetPackageInfo>(packages.Count);
foreach (var pkg in packages)
{
if (pkg.Condition is null)
{
foreach (var tf in projectTfs)
{
result.Add(pkg.WithCondition($"'$(TargetFramework)' == '{tf}'"));
}
}
else
{
result.Add(pkg);
}
}

return result;
}

/// <summary>
/// For unit tests.
/// </summary>
internal List<NuGetPackageInfo> GetPackagesProjectSource(string projectSource)
{
var projectDocument = CreateXmlDocument();
projectDocument.LoadXml(projectSource);
return GetPackagesInProject(projectDocument);
var packages = GetPackagesInProject(projectDocument);
return ApplyTargetFrameworkCondition(packages, GetProjectTargetFrameworks(projectDocument));
}

/// <summary>
Expand All @@ -90,54 +214,52 @@ internal string UpdateProjectFromSource(string projectSource, List<NuGetPackageI

private void RemoveVersionOnPackageReferences(XmlDocument doc, List<NuGetPackageInfo> packages)
{
foreach (var packageId in packages.Select(x => x.Id))
foreach (var packageId in packages.Select(x => x.Id).Distinct())
{
_logger.LogDebug("Locating single PackageReference for Includes = {PackageId}", packageId);
_logger.LogDebug("Locating PackageReferences for Include = {PackageId}", packageId);

if (doc.SelectSingleNode($"//PackageReference[@Include='{packageId}']") is not XmlElement packageReference)
var matches = doc.SelectNodes($"//PackageReference[@Include='{packageId}']");

if (matches is null || matches.Count == 0)
{
_logger.LogInformation("Couldn't find any elements");
continue;
}

var removedVersion = packageReference.Attributes?.Remove(packageReference.Attributes[VersionElementName]);

if (removedVersion is not null)
foreach (XmlNode match in matches)
{
_logger.LogInformation("Removed Version attribute for package {PackageId}", packageId);
continue;
RemoveVersionFromReference((XmlElement)match, packageId);
}
}
}

_logger.LogDebug("No Version attribute found, looking for child element");
var versionElement = packageReference.SelectSingleNode(VersionElementName);
private static void RemoveVersionFromReference(XmlElement packageReference, string packageId)
{
var removedVersion = packageReference.Attributes?.Remove(packageReference.Attributes[VersionElementName]);

if (versionElement is null)
{
_logger.LogInformation("No Version attribute and no Version child element for package {PackageId}",
packageId);
continue;
}
if (removedVersion is not null)
{
return;
}

packageReference.RemoveChild(versionElement);
var versionElement = packageReference.SelectSingleNode(VersionElementName);

// A Version child element means at least two children: one for
// whitespace before the element and then the element itself.
// Remove the leading whitespace.
foreach (var el in packageReference.ChildNodes.OfType<XmlWhitespace>())
{
packageReference.RemoveChild(el);
}
if (versionElement is null)
{
return;
}

// If the PackageReference element only contained a Version
// element, then there will be an additional whitespace child
// element that precedes the closing PackageReference element. In
// this case, remove all whitespace and mark the element as
// self-closing.
if (string.IsNullOrWhiteSpace(packageReference.InnerText))
{
packageReference.InnerXml = string.Empty;
packageReference.IsEmpty = true;
}
packageReference.RemoveChild(versionElement);

foreach (var el in packageReference.ChildNodes.OfType<XmlWhitespace>())
{
packageReference.RemoveChild(el);
}

if (string.IsNullOrWhiteSpace(packageReference.InnerText))
{
packageReference.InnerXml = string.Empty;
packageReference.IsEmpty = true;
}
}

Expand Down Expand Up @@ -206,10 +328,14 @@ private List<NuGetPackageInfo> GetPackagesFromReferences(XmlNodeList packageRefe
packageVersion = version.InnerText;
}

_logger.LogDebug("Found NuGet package {PackageName} version {PackageVersion}",
packageName, packageVersion);
var condition = (packageReference.ParentNode as XmlElement)?.GetAttribute("Condition");
var conditionValue = string.IsNullOrEmpty(condition) ? null : condition;

_logger.LogDebug("Found NuGet package {PackageName} version {PackageVersion}{Condition}",
packageName, packageVersion,
conditionValue is not null ? $" condition: {conditionValue}" : "");

packagesInProject.Add(new NuGetPackageInfo(packageName, packageVersion));
packagesInProject.Add(new NuGetPackageInfo(packageName, packageVersion, conditionValue));
}

return packagesInProject;
Expand Down
17 changes: 13 additions & 4 deletions src/CentralPackageManagementMigrator/MigratorCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,34 @@ internal class MigratorCommand : RootCommand
DefaultValueFactory = _ => LogLevel.Information
};

private readonly Option<DirectoryInfo> _pathOption = new("--path", "-p")
{
Description = "Directory to search for project files under.",
DefaultValueFactory = _ => new DirectoryInfo(Directory.GetCurrentDirectory())
};

public MigratorCommand() : base(CommandDescription)
{
Options.Add(_logLevelOption);
Options.Add(_pathOption);

SetAction(parseResult =>
{
var logLevel = parseResult.GetRequiredValue(_logLevelOption);
return Migrate(logLevel);
var path = parseResult.GetRequiredValue(_pathOption);
return Migrate(logLevel, path);
});
}

private static int Migrate(LogLevel logLevel)
private static int Migrate(LogLevel logLevel, DirectoryInfo? path = null)
{
var exitCode = 0;
LoggingUtility.SetupLogging(logLevel);
var logger = LoggingUtility.CreateLogger<MigratorCommand>();

logger.LogDebug("Called with verbosity: {Level}", logLevel.ToString());

var searchPath = Directory.GetCurrentDirectory();
var searchPath = path?.FullName ?? Directory.GetCurrentDirectory();
logger.LogInformation("Adding central package management under search path: {SearchPath}", searchPath);

var directoryPackagesProps = new PackagesPropsBuilder(LoggingUtility.CreateLogger<PackagesPropsBuilder>(),
Expand All @@ -48,11 +56,12 @@ private static int Migrate(LogLevel logLevel)
else
{
var projectBuilder = new ProjectBuilder(LoggingUtility.CreateLogger<ProjectBuilder>());
var allTargetFrameworks = projectBuilder.GetTargetFrameworks(searchPath);
var packages = projectBuilder.GetPackagesInAllProjects(searchPath);

if (packages.Count > 0)
{
var distinctPackages = packages.ToDistinctOrder();
var distinctPackages = packages.ToDistinctOrder(allTargetFrameworks);

directoryPackagesProps.WriteFile(distinctPackages);
projectBuilder.UpdateProjects(packages);
Expand Down
11 changes: 8 additions & 3 deletions src/CentralPackageManagementMigrator/NuGetPackageInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ internal class NuGetPackageInfo : IEquatable<NuGetPackageInfo>
{
public string Id { get; }
public string Version { get; }
public string? Condition { get; }

public NuGetPackageInfo(string id, string version)
public NuGetPackageInfo(string id, string version, string? condition = null)
{
Id = id;
Version = version;
Condition = condition;
}

public NuGetPackageInfo WithCondition(string? condition) => new(Id, Version, condition);

public override bool Equals(object? obj) => Equals(obj as NuGetPackageInfo);
public bool Equals(NuGetPackageInfo? other)
{
Expand All @@ -25,8 +29,9 @@ public bool Equals(NuGetPackageInfo? other)
}

return Id.Equals(other.Id, StringComparison.InvariantCultureIgnoreCase) &&
Version.Equals(other.Version);
Version.Equals(other.Version) &&
Condition == other.Condition;
}

public override int GetHashCode() => HashCode.Combine(Id.ToLowerInvariant(), Version);
public override int GetHashCode() => HashCode.Combine(Id.ToLowerInvariant(), Version, Condition);
}
Loading