Skip to content

Commit

Permalink
Merge branch 'main' into jamiemage/strong-type-registry-client
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMagee authored Jan 10, 2024
2 parents 3c44314 + d0ef613 commit 92dcaa8
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 26 deletions.
1 change: 1 addition & 0 deletions nuget/helpers/lib/NuGetUpdater/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ root = true
# All files
[*]
indent_style = space
charset = utf-8

# Xml files
[*.xml]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Xunit;

namespace NuGetUpdater.Core.Test.Utilities;
Expand All @@ -17,6 +18,7 @@ public async Task UpdateDependency_UpdatesDependencies((string Path, string Cont
using var directory = TemporaryDirectory.CreateWithContents(startingContents);
var projectPath = Path.Combine(directory.DirectoryPath, startingContents.First().Path);
var logger = new Logger(verbose: false);

// Act
await SdkPackageUpdater.UpdateDependencyAsync(directory.DirectoryPath, projectPath, dependencyName, previousVersion, newDependencyVersion, isTransitive, logger);

Expand Down Expand Up @@ -58,6 +60,95 @@ public static IEnumerable<object[]> GetDependencyUpdates()
"Newtonsoft.Json", "12.0.1", "13.0.1", false // isTransitive
};

// Dependency package has version constraint
yield return new object[]
{
new[]
{
(Path: "src/Project/Project.csproj", Content: """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.3.17.3" />
<PackageReference Include="AWSSDK.Core" Version="3.3.21.19" />
</ItemGroup>
</Project>
"""),
}, // starting contents
new[]
{
// If a dependency has a version constraint outside of our new-version, we don't update anything
(Path: "src/Project/Project.csproj", Content: """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.3.17.3" />
<PackageReference Include="AWSSDK.Core" Version="3.3.21.19" />
</ItemGroup>
</Project>
"""),
},// expected contents
"AWSSDK.Core", "3.3.21.19", "3.7.300.20", false // isTransitive
};

// Dependency project has version constraint
yield return new object[]
{
new[]
{
(Path: "src/Project2/Project2.csproj", Content: """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<ProjectReference Include="../Project/Project.csproj" />
</ItemGroup>
</Project>
"""),
(Path: "src/Project/Project.csproj", Content: """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="[12.0.1, 13.0.0)" />
</ItemGroup>
</Project>
"""),
}, // starting contents
new[]
{
(Path: "src/Project2/Project2.csproj", Content: """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<ProjectReference Include="../Project/Project.csproj" />
</ItemGroup>
</Project>
"""), // starting contents
(Path: "src/Project/Project.csproj", Content: """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="[12.0.1, 13.0.0)" />
</ItemGroup>
</Project>
"""),
},// expected contents
"Newtonsoft.Json", "12.0.1", "13.0.1", false // isTransitive
};

// Multiple references
yield return new object[]
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Threading.Tasks;

using Microsoft.Build.Evaluation;
using Microsoft.Language.Xml;

using NuGet.Versioning;
Expand Down Expand Up @@ -120,6 +121,18 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string proje
await UpdateTopLevelDepdendencyAsync(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, packagesAndVersions, logger);
}

var updatedTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependenyInfos(buildFiles);
foreach (var tfm in tfms)
{
var updatedPackages = await MSBuildHelper.GetAllPackageDependenciesAsync(repoRootPath, projectPath, tfm, updatedTopLevelDependencies.ToArray(), logger);
var dependenciesAreCoherent = await MSBuildHelper.DependenciesAreCoherentAsync(repoRootPath, projectPath, tfm, updatedPackages, logger);
if (!dependenciesAreCoherent)
{
logger.Log($" Package [{dependencyName}] could not be updated in [{projectPath}] because it would cause a dependency conflict.");
return;
}
}

foreach (var buildFile in buildFiles)
{
if (await buildFile.SaveAsync())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;

using Microsoft.Build.Construction;
using Microsoft.Build.Definition;
Expand Down Expand Up @@ -47,7 +49,7 @@ public static string[] GetTargetFrameworkMonikers(ImmutableArray<ProjectBuildFil

foreach (var buildFile in buildFiles)
{
var projectRoot = ProjectRootElement.Open(buildFile.Path);
var projectRoot = CreateProjectRootElement(buildFile);

foreach (var property in projectRoot.Properties)
{
Expand Down Expand Up @@ -140,7 +142,7 @@ public static IEnumerable<Dependency> GetTopLevelPackageDependenyInfos(Immutable

foreach (var buildFile in buildFiles)
{
var projectRoot = ProjectRootElement.Open(buildFile.Path);
var projectRoot = CreateProjectRootElement(buildFile);

foreach (var packageItem in projectRoot.Items
.Where(i => (i.ItemType == "PackageReference" || i.ItemType == "GlobalPackageReference")))
Expand Down Expand Up @@ -239,27 +241,52 @@ public static bool TryGetPropertyName(string versionContent, [NotNullWhen(true)]
return false;
}

internal static async Task<Dependency[]> GetAllPackageDependenciesAsync(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger? logger = null)
internal static async Task<bool> DependenciesAreCoherentAsync(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger logger)
{
var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-resolution_");
var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
try
{
var projectDirectory = Path.GetDirectoryName(projectPath);
projectDirectory ??= repoRoot;
var topLevelFiles = Directory.GetFiles(repoRoot);
var nugetConfigPath = PathHelper.GetFileInDirectoryOrParent(projectDirectory, repoRoot, "NuGet.Config", caseSensitive: false);
if (nugetConfigPath is not null)
{
File.Copy(nugetConfigPath, Path.Combine(tempDirectory.FullName, "NuGet.Config"));
}
var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\"");

// NU1608: Detected package version outside of dependency constraint

return exitCode == 0 && !stdOut.Contains("NU1608");
}
finally
{
tempDirectory.Delete(recursive: true);
}
}

private static ProjectRootElement CreateProjectRootElement(ProjectBuildFile buildFile)
{
var xmlString = buildFile.Contents.ToFullString();
using var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlString));
using var xmlReader = XmlReader.Create(xmlStream);
var projectRoot = ProjectRootElement.Create(xmlReader);

return projectRoot;
}

var packageReferences = string.Join(
Environment.NewLine,
packages
.Where(p => !string.IsNullOrWhiteSpace(p.Version)) // empty `Version` attributes will cause the temporary project to not build
.Select(static p => $"<PackageReference Include=\"{p.Name}\" Version=\"[{p.Version}]\" />"));
private static async Task<string> CreateTempProjectAsync(DirectoryInfo tempDir, string repoRoot, string projectPath, string targetFramework, Dependency[] packages)
{
var projectDirectory = Path.GetDirectoryName(projectPath);
projectDirectory ??= repoRoot;
var topLevelFiles = Directory.GetFiles(repoRoot);
var nugetConfigPath = PathHelper.GetFileInDirectoryOrParent(projectDirectory, repoRoot, "NuGet.Config", caseSensitive: false);
if (nugetConfigPath is not null)
{
File.Copy(nugetConfigPath, Path.Combine(tempDir.FullName, "NuGet.Config"));
}

var projectContents = $"""
var packageReferences = string.Join(
Environment.NewLine,
packages
.Where(p => !string.IsNullOrWhiteSpace(p.Version)) // empty `Version` attributes will cause the temporary project to not build
.Select(static p => $"<PackageReference Include=\"{p.Name}\" Version=\"[{p.Version}]\" />"));

var projectContents = $"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>{targetFramework}</TargetFramework>
Expand Down Expand Up @@ -287,13 +314,24 @@ internal static async Task<Dependency[]> GetAllPackageDependenciesAsync(string r
</Target>
</Project>
""";
var tempProjectPath = Path.Combine(tempDirectory.FullName, "Project.csproj");
await File.WriteAllTextAsync(tempProjectPath, projectContents);
var tempProjectPath = Path.Combine(tempDir.FullName, "Project.csproj");
await File.WriteAllTextAsync(tempProjectPath, projectContents);

// prevent directory crawling
await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Build.props"), "<Project />");
await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Build.targets"), "<Project />");
await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Packages.props"), "<Project />");

// prevent directory crawling
await File.WriteAllTextAsync(Path.Combine(tempDirectory.FullName, "Directory.Build.props"), "<Project />");
await File.WriteAllTextAsync(Path.Combine(tempDirectory.FullName, "Directory.Build.targets"), "<Project />");
await File.WriteAllTextAsync(Path.Combine(tempDirectory.FullName, "Directory.Packages.props"), "<Project />");
return tempProjectPath;
}

internal static async Task<Dependency[]> GetAllPackageDependenciesAsync(
string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger? logger = null)
{
var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-resolution_");
try
{
var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);

var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\" /t:_ReportDependencies");

Expand All @@ -306,6 +344,7 @@ internal static async Task<Dependency[]> GetAllPackageDependenciesAsync(string r
.Where(match => match.Success)
.Select(match => new Dependency(match.Groups["PackageName"].Value, match.Groups["PackageVersion"].Value, DependencyType.Unknown))
.ToArray();

return allDependencies;
}
else
Expand All @@ -318,7 +357,7 @@ internal static async Task<Dependency[]> GetAllPackageDependenciesAsync(string r
{
try
{
Directory.Delete(tempDirectory.FullName, true);
tempDirectory.Delete(recursive: true);
}
catch
{
Expand Down
2 changes: 1 addition & 1 deletion nuget/lib/dependabot/nuget/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def project_file_parser
end

def project_files
projfile = /\.[a-z]{2}proj$/
projfile = /\.([a-z]{2})?proj$/
packageprops = /[Dd]irectory.[Pp]ackages.props/

dependency_files.select do |df|
Expand Down
62 changes: 62 additions & 0 deletions nuget/spec/dependabot/nuget/file_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,68 @@ def dependencies_from_info(deps_info)
let(:dependencies) { parser.parse }
subject(:top_level_dependencies) { dependencies.select(&:top_level?) }

context "with a .proj file" do
let(:files) { [proj_file] }
let(:proj_file) do
Dependabot::DependencyFile.new(
name: "proj.proj",
content: fixture("csproj", "basic2.csproj")
)
end

let(:proj_dependencies) do
[
{ name: "Microsoft.Extensions.DependencyModel", version: "1.0.1", file: "proj.proj" },
{ name: "Serilog", version: "2.3.0", file: "proj.proj" }
]
end

before do
dummy_project_file_parser = instance_double(described_class::ProjectFileParser)
allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser)
allow(dummy_project_file_parser).to receive(:dependency_set).with(project_file: proj_file).and_return(
dependencies_from_info(proj_dependencies)
)
end
its(:length) { is_expected.to eq(2) }

describe "the first dependency" do
subject(:dependency) { top_level_dependencies.first }

it "has the right details" do
expect(dependency).to be_a(Dependabot::Dependency)
expect(dependency.name).to eq("Microsoft.Extensions.DependencyModel")
expect(dependency.version).to eq("1.0.1")
expect(dependency.requirements).to eq(
[{
requirement: "1.0.1",
file: "proj.proj",
groups: ["dependencies"],
source: nil
}]
)
end
end

describe "the last dependency" do
subject(:dependency) { top_level_dependencies.last }

it "has the right details" do
expect(dependency).to be_a(Dependabot::Dependency)
expect(dependency.name).to eq("Serilog")
expect(dependency.version).to eq("2.3.0")
expect(dependency.requirements).to eq(
[{
requirement: "2.3.0",
file: "proj.proj",
groups: ["dependencies"],
source: nil
}]
)
end
end
end

context "with a single project file" do
let(:project_dependencies) do
[
Expand Down

0 comments on commit 92dcaa8

Please sign in to comment.