Skip to content
This repository has been archived by the owner on Jul 12, 2022. It is now read-only.

Commit

Permalink
Failure to update .NET AspNet WebApi projects (#242)
Browse files Browse the repository at this point in the history
* Patch WebApiProjects with a Condition on Import

This allows the dotnet remove/add commands to work without trying to import a non-existent project

* Follow project references when updating import conditions

* Undo unnecessary change

* Specify solution file explicitly in build

This might help the random build errors, according to dotnet/sdk#2076
  • Loading branch information
skolima authored Apr 30, 2018
1 parent 8ae51bb commit 2926273
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 30 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,4 @@ _ReSharper*/

# Custom
NuKeeper/Properties/launchSettings.json


*.orig
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mono: none
dist: trusty
dotnet: 2.1.105
script:
- dotnet build -c Release -f netcoreapp2.0
- dotnet build -c Release -f netcoreapp2.0 NuKeeper.sln /m:1
- dotnet test -c Release -f netcoreapp2.0 NuKeeper.Tests/NuKeeper.Tests.csproj --filter "TestCategory!=WindowsOnly"
- dotnet test -c Release -f netcoreapp2.0 NuKeeper.Inspection.Tests/NuKeeper.Inspection.Tests.csproj --filter "TestCategory!=WindowsOnly"
- dotnet test -c Release -f netcoreapp2.0 NuKeeper.Integration.Tests/NuKeeper.Integration.Tests.csproj --filter "TestCategory!=WindowsOnly"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ public class DotNetUpdatePackageCommandTests
<VSToolsPath Condition=""'$(VSToolsPath)' == ''"">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<!-- without the second package, 'dotnet add' will refuse to run -->
<!-- as the heuristic will consider this a packages.config project -->
<PackageReference Include=""Newtonsoft.Json"" Version=""11.0.2"" />
</ItemGroup>
<Import Project=""$(MSBuildBinPath)\Microsoft.CSharp.targets"" />
<Import Project=""$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets"" Condition=""'$(VSToolsPath)' != ''"" />
<Import Project=""$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets"" Condition=""Exists('$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets')"" />
</Project>";

private readonly string _testDotNetCoreProject =
Expand Down Expand Up @@ -55,7 +60,6 @@ public class DotNetUpdatePackageCommandTests
</Project>";

[Test]
[Ignore("Known failure, issue #239")]
public async Task ShouldNotThrowOnWebProjectMixedStyleUpdates()
{
await ExecuteValidUpdateTest(_testWebApiProject);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.IO;
using System.Threading.Tasks;
using NuKeeper.Inspection.RepositoryInspection;
using NuKeeper.NuGet.Process;
using NUnit.Framework;

namespace NuKeeper.Integration.Tests.NuGet.Process
{
[TestFixture]
public class UpdateProjectImportsCommandTests
{
private readonly string _testWebApiProject =
@"<Project xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
<Import Project=""$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"" Condition=""Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"" />
<Import Project=""$(MSBuildBinPath)\Microsoft.CSharp.targets"" />
<Import Project=""$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets"" Condition=""'$(VSToolsPath)' != ''"" />
<Import Project=""$(VSToolsPath)\DummyImportWithoutCondition\Microsoft.WebApplication.targets"" />
</Project>";

private readonly string _projectWithReference =
@"<Project xmlns=""http://schemas.microsoft.com/developer/msbuild/2003""><ItemGroup><ProjectReference Include=""{importPath}"" /></ItemGroup></Project>";

private readonly string _unpatchedImport =
@"<Import Project=""$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets"" Condition=""'$(VSToolsPath)' != ''"" />";

private readonly string _patchedImport =
@"<Import Project=""$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets"" Condition=""Exists('$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets')"" />";

[Test]
public async Task ShouldUpdateConditionOnTaskImport()
{
var workDirectory = Path.Combine(TestContext.CurrentContext.WorkDirectory,
nameof(ShouldUpdateConditionOnTaskImport));

Directory.CreateDirectory(workDirectory);
var projectName = nameof(ShouldUpdateConditionOnTaskImport) + ".csproj";
var projectPath = Path.Combine(workDirectory, projectName);
await File.WriteAllTextAsync(projectPath, _testWebApiProject);

var subject = new UpdateProjectImportsCommand();

await subject.Invoke(null, null,
new PackageInProject("acme", "1",
new PackagePath(workDirectory, projectName, PackageReferenceType.ProjectFileOldStyle)));

var updatedContents = await File.ReadAllTextAsync(projectPath);

Assert.That(updatedContents, Does.Not.Contain(_unpatchedImport));
Assert.That(updatedContents, Does.Contain(_patchedImport));
}

[Test]
public async Task ShouldFollowResolvableImports()
{
var workDirectory = Path.Combine(TestContext.CurrentContext.WorkDirectory,
nameof(ShouldFollowResolvableImports));

Directory.CreateDirectory(workDirectory);

var projectName = nameof(ShouldFollowResolvableImports) + ".csproj";
var projectPath = Path.Combine(workDirectory, projectName);
await File.WriteAllTextAsync(projectPath, _testWebApiProject);

var intermediateProject = Path.Combine(workDirectory, "Intermediate.csproj");
var intermediateContents = _projectWithReference.Replace("{importPath}", projectPath);
await File.WriteAllTextAsync(intermediateProject, intermediateContents);

var rootProject = Path.Combine(workDirectory, "RootProject.csproj");
var rootContets = _projectWithReference.Replace("{importPath}",
Path.Combine("..", nameof(ShouldFollowResolvableImports), "Intermediate.csproj"));
await File.WriteAllTextAsync(rootProject, rootContets);

var subject = new UpdateProjectImportsCommand();

await subject.Invoke(null, null,
new PackageInProject("acme", "1",
new PackagePath(workDirectory, "RootProject.csproj", PackageReferenceType.ProjectFileOldStyle)));

var updatedContents = await File.ReadAllTextAsync(projectPath);

Assert.That(updatedContents, Does.Not.Contain(_unpatchedImport));
Assert.That(updatedContents, Does.Contain(_patchedImport));
}
}
}
46 changes: 25 additions & 21 deletions NuKeeper/Engine/Packages/PackageUpdater.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NuKeeper.Configuration;
using NuKeeper.Git;
Expand Down Expand Up @@ -64,35 +65,38 @@ private async Task UpdateAllCurrentUsages(PackageUpdateSet updateSet)
{
foreach (var current in updateSet.CurrentPackages)
{
var restoreCommand = GetRestoreCommand(current.Path.PackageReferenceType);
if (restoreCommand != null)
var updateCommands = GetUpdateCommands(current.Path.PackageReferenceType);
foreach (var updateCommand in updateCommands)
{
await restoreCommand.Invoke(current.Path.Info);
await updateCommand.Invoke(updateSet.SelectedVersion, updateSet.Selected.Source, current);
}

var updateCommand = GetUpdateCommand(current.Path.PackageReferenceType);
await updateCommand.Invoke(updateSet.SelectedVersion, updateSet.Selected.Source, current);
}
}

private IFileRestoreCommand GetRestoreCommand(PackageReferenceType packageReferenceType)
{
if (packageReferenceType != PackageReferenceType.ProjectFile)
{
return new NuGetFileRestoreCommand(_logger, _settings);
}

return null;
}

private IUpdatePackageCommand GetUpdateCommand(PackageReferenceType packageReferenceType)
private IReadOnlyCollection<IPackageCommand> GetUpdateCommands(PackageReferenceType packageReferenceType)
{
if (packageReferenceType != PackageReferenceType.PackagesConfig)
switch (packageReferenceType)
{
return new DotNetUpdatePackageCommand(_logger, _settings);
case PackageReferenceType.PackagesConfig:
return new IPackageCommand[]
{
new NuGetFileRestoreCommand(_logger, _settings),
new NuGetUpdatePackageCommand(_logger, _settings)
};

case PackageReferenceType.ProjectFileOldStyle:
return new IPackageCommand[]
{
new UpdateProjectImportsCommand(),
new NuGetFileRestoreCommand(_logger, _settings),
new DotNetUpdatePackageCommand(_logger, _settings)
};

case PackageReferenceType.ProjectFile:
return new[] {new DotNetUpdatePackageCommand(_logger, _settings)};

default: throw new ArgumentOutOfRangeException(nameof(packageReferenceType));
}

return new NuGetUpdatePackageCommand(_logger, _settings);
}

private async Task MakeGitHubPullRequest(
Expand Down
2 changes: 1 addition & 1 deletion NuKeeper/NuGet/Process/DotNetUpdatePackageCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace NuKeeper.NuGet.Process
{
public class DotNetUpdatePackageCommand : IUpdatePackageCommand
public class DotNetUpdatePackageCommand : IPackageCommand
{
private readonly IExternalProcess _externalProcess;
private readonly INuKeeperLogger _logger;
Expand Down
2 changes: 1 addition & 1 deletion NuKeeper/NuGet/Process/IFileRestoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace NuKeeper.NuGet.Process
{
public interface IFileRestoreCommand
public interface IFileRestoreCommand : IPackageCommand
{
Task Invoke(FileInfo file);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace NuKeeper.NuGet.Process
{
public interface IUpdatePackageCommand
public interface IPackageCommand
{
Task Invoke(NuGetVersion newVersion, string packageSource, PackageInProject currentPackage);
}
Expand Down
7 changes: 7 additions & 0 deletions NuKeeper/NuGet/Process/NuGetFileRestoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using NuGet.Versioning;
using NuKeeper.Configuration;
using NuKeeper.Inspection.Formats;
using NuKeeper.Inspection.Logging;
using NuKeeper.Inspection.RepositoryInspection;
using NuKeeper.ProcessRunner;

namespace NuKeeper.NuGet.Process
Expand Down Expand Up @@ -61,6 +63,11 @@ public async Task Invoke(FileInfo file)
}
}

public async Task Invoke(NuGetVersion selectedVersion, string source, PackageInProject current)
{
await Invoke(current.Path.Info);
}

private static string GetSourcesCommandLine(IEnumerable<string> sources)
{
return sources.Select(s => $"-Source {s}").JoinWithSeparator(" ");
Expand Down
2 changes: 1 addition & 1 deletion NuKeeper/NuGet/Process/NuGetUpdatePackageCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace NuKeeper.NuGet.Process
{
public class NuGetUpdatePackageCommand : IUpdatePackageCommand
public class NuGetUpdatePackageCommand : IPackageCommand
{
private readonly IExternalProcess _externalProcess;
private readonly INuKeeperLogger _logger;
Expand Down
94 changes: 94 additions & 0 deletions NuKeeper/NuGet/Process/UpdateProjectImportsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using NuGet.Versioning;
using NuKeeper.Inspection.RepositoryInspection;

namespace NuKeeper.NuGet.Process
{
public class UpdateProjectImportsCommand : IPackageCommand
{
public async Task Invoke(NuGetVersion newVersion, string packageSource, PackageInProject currentPackage)
{
var projectsToUpdate = new Stack<string>();
projectsToUpdate.Push(currentPackage.Path.FullName);

while (projectsToUpdate.TryPop(out var currentProject))
{
using (var projectContents = File.Open(currentProject, FileMode.Open, FileAccess.ReadWrite))
{
var projectsToCheck = await UpdateConditionsOnProjects(projectContents);
foreach (var potentialProject in projectsToCheck)
{
var fullPath =
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(currentProject), potentialProject));
if (File.Exists(fullPath))
{
projectsToUpdate.Push(fullPath);
}
}
}
}
}

private async Task<IEnumerable<string>> UpdateConditionsOnProjects(Stream fileContents)
{
var xml = XDocument.Load(fileContents);
var ns = xml.Root.GetDefaultNamespace();

var project = xml.Element(ns + "Project");

if (project == null)
{
return Enumerable.Empty<string>();
}

var imports = project.Elements(ns + "Import");
var importsWithToolsPath = imports
.Where(i => i.Attributes("Project").Any(a => a.Value.Contains("$(VSToolsPath)"))).ToList();
var importsWithoutCondition = importsWithToolsPath.Where(i => !i.Attributes("Condition").Any());
var importsWithBrokenVsToolsCondition = importsWithToolsPath.Where(i =>
i.Attributes("Condition").Any(a => a.Value == "\'$(VSToolsPath)\' != \'\'"));

var saveRequired = false;
foreach (var importToFix in importsWithBrokenVsToolsCondition.Concat(importsWithoutCondition))
{
saveRequired = true;
UpdateImportNode(importToFix);
}

if (saveRequired)
{
fileContents.Seek(0, SeekOrigin.Begin);
await xml.SaveAsync(fileContents, SaveOptions.None, CancellationToken.None);
}

return FindProjectReferences(project, ns);
}

private static IEnumerable<string> FindProjectReferences(XElement project, XNamespace ns)
{
var itemGroups = project.Elements(ns + "ItemGroup");
var projectReferences = itemGroups.SelectMany(ig => ig.Elements(ns + "ProjectReference"));
var includes = projectReferences.Attributes("Include").Select(a => a.Value);
return includes;
}

private static void UpdateImportNode(XElement importToFix)
{
var importPath = importToFix.Attribute("Project").Value;
var condition = $"Exists('{importPath}')";
if (!importToFix.Attributes("Condition").Any())
{
importToFix.Add(new XAttribute("Condition", condition));
}
else
{
importToFix.Attribute("Condition").Value = condition;
}
}
}
}

0 comments on commit 2926273

Please sign in to comment.