diff --git a/TestAssets/NonRestoredTestProjects/DotnetAddP2PProjects/WithDoubledRef/WithDoubledRef.cs b/TestAssets/NonRestoredTestProjects/DotnetAddP2PProjects/WithDoubledRef/WithDoubledRef.cs
new file mode 100644
index 000000000..0ba94d2f9
--- /dev/null
+++ b/TestAssets/NonRestoredTestProjects/DotnetAddP2PProjects/WithDoubledRef/WithDoubledRef.cs
@@ -0,0 +1,3 @@
+public class WithDoubledRef
+{
+}
\ No newline at end of file
diff --git a/TestAssets/NonRestoredTestProjects/DotnetAddP2PProjects/WithDoubledRef/WithDoubledRef.csproj b/TestAssets/NonRestoredTestProjects/DotnetAddP2PProjects/WithDoubledRef/WithDoubledRef.csproj
new file mode 100644
index 000000000..a60b1e748
--- /dev/null
+++ b/TestAssets/NonRestoredTestProjects/DotnetAddP2PProjects/WithDoubledRef/WithDoubledRef.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Library
+ net451;netcoreapp1.0
+
+
+
+
+
+
+
+ 1.0.0-alpha-20161104-2
+ All
+
+
+
+
+ 1.0.1
+
+
+
+
+
+
+
+
diff --git a/build/test/TestProjects.targets b/build/test/TestProjects.targets
index b263c5c9a..60fce21dc 100644
--- a/build/test/TestProjects.targets
+++ b/build/test/TestProjects.targets
@@ -51,7 +51,8 @@
Include="test$(PathSeparator)ArgumentsReflector$(PathSeparator)ArgumentsReflector.csproj;
test$(PathSeparator)Microsoft.DotNet.Tools.Tests.Utilities$(PathSeparator)Microsoft.DotNet.Tools.Tests.Utilities.csproj;
test$(PathSeparator)TestingAbstractions$(PathSeparator)TestAppWithFullPdbs$(PathSeparator)TestAppWithFullPdbs.csproj;
- test$(PathSeparator)TestingAbstractions$(PathSeparator)TestAppWithPortablePdbs$(PathSeparator)TestAppWithPortablePdbs.csproj" />
+ test$(PathSeparator)TestingAbstractions$(PathSeparator)TestAppWithPortablePdbs$(PathSeparator)TestAppWithPortablePdbs.csproj;
+ test$(PathSeparator)Msbuild.Tests.Utilities$(PathSeparator)Msbuild.Tests.Utilities.csproj" />
diff --git a/src/dotnet/DispatchCommand.cs b/src/dotnet/DispatchCommand.cs
new file mode 100644
index 000000000..7878fcfdc
--- /dev/null
+++ b/src/dotnet/DispatchCommand.cs
@@ -0,0 +1,76 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.DotNet.Cli.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.DotNet.Tools
+{
+ public abstract class DispatchCommand
+ {
+ protected abstract string HelpText { get; }
+ protected abstract Dictionary> BuiltInCommands { get; }
+
+ public int Run(string[] args)
+ {
+ DebugHelper.HandleDebugSwitch(ref args);
+
+ if (args.Length == 0 || args[0] == "--help" || args[0] == "-h")
+ {
+ Reporter.Output.WriteLine(HelpText);
+ return 0;
+ }
+
+ string commandObject;
+ string command;
+ if (IsValidCommandName(args[0]))
+ {
+ command = args[0];
+ commandObject = GetCurrentDirectoryWithDirSeparator();
+ args = args.Skip(1).Prepend(commandObject).ToArray();
+ }
+ else if (args.Length == 1)
+ {
+ Reporter.Error.WriteLine(string.Format(LocalizableStrings.RequiredArgumentNotPassed, "").Red());
+ Reporter.Output.WriteLine(HelpText);
+ return 1;
+ }
+ else
+ {
+ commandObject = args[0];
+ command = args[1];
+
+ args = args.Skip(2).Prepend(commandObject).ToArray();
+ }
+
+ Func builtin;
+ if (BuiltInCommands.TryGetValue(command, out builtin))
+ {
+ return builtin(args);
+ }
+
+ Reporter.Error.WriteLine(string.Format(LocalizableStrings.RequiredArgumentIsInvalid, "").Red());
+ Reporter.Output.WriteLine(HelpText);
+ return 1;
+ }
+
+ private bool IsValidCommandName(string s)
+ {
+ return BuiltInCommands.ContainsKey(s);
+ }
+
+ private static string GetCurrentDirectoryWithDirSeparator()
+ {
+ string ret = Directory.GetCurrentDirectory();
+ if (ret[ret.Length - 1] != Path.DirectorySeparatorChar)
+ {
+ ret += Path.DirectorySeparatorChar;
+ }
+
+ return ret;
+ }
+ }
+}
diff --git a/src/dotnet/LocalizableStrings.cs b/src/dotnet/LocalizableStrings.cs
index 64ec79d3f..5a7a4f6a8 100644
--- a/src/dotnet/LocalizableStrings.cs
+++ b/src/dotnet/LocalizableStrings.cs
@@ -22,6 +22,9 @@ namespace Microsoft.DotNet.Tools
public const string ProjectAlreadyHasAreference = "Project already has a reference to `{0}`.";
public const string ReferenceAddedToTheProject = "Reference `{0}` added to the project.";
public const string ReferenceDoesNotExist = "Reference `{0}` does not exist.";
- public const string SpecifyAtLeastOneReference = "You must specify at least one reference to add.";
+ public const string SpecifyAtLeastOneReferenceToAdd = "You must specify at least one reference to add.";
+ public const string SpecifyAtLeastOneReferenceToRemove = "You must specify at least one reference to remove.";
+ public const string ProjectReferenceCouldNotBeFound = "Project reference `{0}` could not be found.";
+ public const string ProjectReferenceRemoved = "Project reference `{0}` removed.";
}
}
diff --git a/src/dotnet/MsbuildProject.cs b/src/dotnet/MsbuildProject.cs
new file mode 100644
index 000000000..0d0f06473
--- /dev/null
+++ b/src/dotnet/MsbuildProject.cs
@@ -0,0 +1,229 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Build.Construction;
+using Microsoft.Build.Evaluation;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.DotNet.Tools.Common;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.DotNet.Tools
+{
+ internal class MsbuildProject
+ {
+ const string ProjectItemElementType = "ProjectReference";
+
+ public ProjectRootElement Project { get; private set; }
+ public string ProjectDirectory { get; private set; }
+
+ private MsbuildProject(ProjectRootElement project)
+ {
+ Project = project;
+ ProjectDirectory = PathUtility.EnsureTrailingSlash(Project.DirectoryPath);
+ }
+
+ public static MsbuildProject FromFileOrDirectory(string fileOrDirectory)
+ {
+ if (File.Exists(fileOrDirectory))
+ {
+ return FromFile(fileOrDirectory);
+ }
+ else
+ {
+ return FromDirectory(fileOrDirectory);
+ }
+ }
+
+ public static MsbuildProject FromFile(string projectPath)
+ {
+ if (!File.Exists(projectPath))
+ {
+ throw new GracefulException(LocalizableStrings.ProjectDoesNotExist, projectPath);
+ }
+
+ var project = TryOpenProject(projectPath);
+ if (project == null)
+ {
+ throw new GracefulException(LocalizableStrings.ProjectIsInvalid, projectPath);
+ }
+
+ return new MsbuildProject(project);
+ }
+
+ public static MsbuildProject FromDirectory(string projectDirectory)
+ {
+ DirectoryInfo dir;
+ try
+ {
+ dir = new DirectoryInfo(projectDirectory);
+ }
+ catch (ArgumentException)
+ {
+ throw new GracefulException(LocalizableStrings.CouldNotFindProjectOrDirectory, projectDirectory);
+ }
+
+ if (!dir.Exists)
+ {
+ throw new GracefulException(LocalizableStrings.CouldNotFindProjectOrDirectory, projectDirectory);
+ }
+
+ FileInfo[] files = dir.GetFiles("*proj");
+ if (files.Length == 0)
+ {
+ throw new GracefulException(LocalizableStrings.CouldNotFindAnyProjectInDirectory, projectDirectory);
+ }
+
+ if (files.Length > 1)
+ {
+ throw new GracefulException(LocalizableStrings.MoreThanOneProjectInDirectory, projectDirectory);
+ }
+
+ FileInfo projectFile = files.First();
+
+ if (!projectFile.Exists)
+ {
+ throw new GracefulException(LocalizableStrings.CouldNotFindAnyProjectInDirectory, projectDirectory);
+ }
+
+ var project = TryOpenProject(projectFile.FullName);
+ if (project == null)
+ {
+ throw new GracefulException(LocalizableStrings.FoundInvalidProject, projectFile.FullName);
+ }
+
+ return new MsbuildProject(project);
+ }
+
+ public int AddProjectToProjectReferences(string framework, IEnumerable refs)
+ {
+ int numberOfAddedReferences = 0;
+
+ ProjectItemGroupElement itemGroup = Project.FindUniformOrCreateItemGroupWithCondition(ProjectItemElementType, framework);
+ foreach (var @ref in refs.Select((r) => NormalizeSlashes(r)))
+ {
+ if (Project.HasExistingItemWithCondition(framework, @ref))
+ {
+ Reporter.Output.WriteLine(string.Format(LocalizableStrings.ProjectAlreadyHasAreference, @ref));
+ continue;
+ }
+
+ numberOfAddedReferences++;
+ itemGroup.AppendChild(Project.CreateItemElement(ProjectItemElementType, @ref));
+
+ Reporter.Output.WriteLine(string.Format(LocalizableStrings.ReferenceAddedToTheProject, @ref));
+ }
+
+ return numberOfAddedReferences;
+ }
+
+ public int RemoveProjectToProjectReferences(string framework, IEnumerable refs)
+ {
+ int totalNumberOfRemovedReferences = 0;
+
+ foreach (var @ref in refs)
+ {
+ totalNumberOfRemovedReferences += RemoveProjectToProjectReferenceAlternatives(framework, @ref);
+ }
+
+ return totalNumberOfRemovedReferences;
+ }
+
+ public void ConvertPathsToRelative(ref List references)
+ {
+ references = references.Select((r) => PathUtility.GetRelativePath(ProjectDirectory, Path.GetFullPath(r))).ToList();
+ }
+
+ public static string NormalizeSlashes(string path)
+ {
+ return path.Replace('/', '\\');
+ }
+
+ public static void EnsureAllReferencesExist(List references)
+ {
+ var notExisting = new List();
+ foreach (var r in references)
+ {
+ if (!File.Exists(r))
+ {
+ notExisting.Add(r);
+ }
+ }
+
+ if (notExisting.Count > 0)
+ {
+ throw new GracefulException(
+ string.Join(
+ Environment.NewLine,
+ notExisting.Select((r) => string.Format(LocalizableStrings.ReferenceDoesNotExist, r))));
+ }
+ }
+
+ private int RemoveProjectToProjectReferenceAlternatives(string framework, string reference)
+ {
+ int numberOfRemovedRefs = 0;
+ foreach (var r in GetIncludeAlternativesForRemoval(reference))
+ {
+ foreach (var existingItem in Project.FindExistingItemsWithCondition(framework, r))
+ {
+ ProjectElementContainer itemGroup = existingItem.Parent;
+ itemGroup.RemoveChild(existingItem);
+ if (itemGroup.Children.Count == 0)
+ {
+ itemGroup.Parent.RemoveChild(itemGroup);
+ }
+
+ numberOfRemovedRefs++;
+ Reporter.Output.WriteLine(string.Format(LocalizableStrings.ProjectReferenceRemoved, r));
+ }
+ }
+
+ if (numberOfRemovedRefs == 0)
+ {
+ Reporter.Output.WriteLine(string.Format(LocalizableStrings.ProjectReferenceCouldNotBeFound, reference));
+ }
+
+ return numberOfRemovedRefs;
+ }
+
+ // Easiest way to explain rationale for this function is on the example. Let's consider following directory structure:
+ // .../a/b/p.proj
+ // .../a/d/ref.proj
+ // .../a/e/f/
+ // Project = /some/path/a/b/p.proj
+ //
+ // We do not know the format of passed reference so
+ // path references to consider for removal are following:
+ // - full path to ref.proj [/some/path/a/d/ref.proj]
+ // - string which is passed as reference is relative to project [../d/ref.proj]
+ // - string which is passed as reference is relative to current dir [../../d/ref.proj]
+ private IEnumerable GetIncludeAlternativesForRemoval(string reference)
+ {
+ // We do not care about duplicates in case when i.e. reference is already full path
+ var ret = new List();
+ ret.Add(reference);
+
+ string fullPath = Path.GetFullPath(reference);
+ ret.Add(fullPath);
+ ret.Add(PathUtility.GetRelativePath(ProjectDirectory, fullPath));
+
+ return ret;
+ }
+
+ // There is ProjectRootElement.TryOpen but it does not work as expected
+ // I.e. it returns null for some valid projects
+ private static ProjectRootElement TryOpenProject(string filename)
+ {
+ try
+ {
+ return ProjectRootElement.Open(filename, new ProjectCollection(), preserveFormatting: true);
+ }
+ catch (Microsoft.Build.Exceptions.InvalidProjectFileException)
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/dotnet/commands/dotnet-add-p2p/MsbuildProjectExtensions.cs b/src/dotnet/MsbuildProjectExtensions.cs
similarity index 81%
rename from src/dotnet/commands/dotnet-add-p2p/MsbuildProjectExtensions.cs
rename to src/dotnet/MsbuildProjectExtensions.cs
index 6385bd40a..8abffa674 100644
--- a/src/dotnet/commands/dotnet-add-p2p/MsbuildProjectExtensions.cs
+++ b/src/dotnet/MsbuildProjectExtensions.cs
@@ -1,9 +1,12 @@
-using Microsoft.Build.Construction;
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Build.Construction;
using Microsoft.DotNet.ProjectJsonMigration;
using System.Collections.Generic;
using System.Linq;
-namespace Microsoft.DotNet.Tools.Add.ProjectToProjectReference
+namespace Microsoft.DotNet.Tools
{
internal static class MsbuildProjectExtensions
{
@@ -66,7 +69,16 @@ namespace Microsoft.DotNet.Tools.Add.ProjectToProjectReference
public static bool HasInclude(this ProjectItemElement el, string include)
{
- return el.Includes().Contains(include);
+ include = NormalizeIncludeForComparison(include);
+ foreach (var i in el.Includes())
+ {
+ if (include == NormalizeIncludeForComparison(i))
+ {
+ return true;
+ }
+ }
+
+ return false;
}
private static bool TryGetFrameworkConditionString(string framework, out string condition)
@@ -80,5 +92,10 @@ namespace Microsoft.DotNet.Tools.Add.ProjectToProjectReference
condition = $"'$(TargetFramework)' == '{framework}'";
return true;
}
+
+ private static string NormalizeIncludeForComparison(string include)
+ {
+ return MsbuildProject.NormalizeSlashes(include.ToLower());
+ }
}
}
diff --git a/src/dotnet/Program.cs b/src/dotnet/Program.cs
index bd802401e..4072ab395 100644
--- a/src/dotnet/Program.cs
+++ b/src/dotnet/Program.cs
@@ -18,6 +18,7 @@ using Microsoft.DotNet.Tools.New;
using Microsoft.DotNet.Tools.NuGet;
using Microsoft.DotNet.Tools.Pack;
using Microsoft.DotNet.Tools.Publish;
+using Microsoft.DotNet.Tools.Remove;
using Microsoft.DotNet.Tools.Restore;
using Microsoft.DotNet.Tools.RestoreProjectJson;
using Microsoft.DotNet.Tools.Run;
@@ -31,7 +32,7 @@ namespace Microsoft.DotNet.Cli
{
private static Dictionary> s_builtIns = new Dictionary>
{
- ["add"] = AddCommand.Run,
+ ["add"] = (new AddCommand()).Run,
["build"] = BuildCommand.Run,
["clean"] = CleanCommand.Run,
["help"] = HelpCommand.Run,
@@ -41,6 +42,7 @@ namespace Microsoft.DotNet.Cli
["nuget"] = NuGetCommand.Run,
["pack"] = PackCommand.Run,
["publish"] = PublishCommand.Run,
+ ["remove"] = (new RemoveCommand()).Run,
["restore"] = RestoreCommand.Run,
["restore-projectjson"] = RestoreProjectJsonCommand.Run,
["run"] = RunCommand.Run,
diff --git a/src/dotnet/commands/dotnet-add-p2p/Program.cs b/src/dotnet/commands/dotnet-add-p2p/Program.cs
index 6a724620d..c6ada41cc 100644
--- a/src/dotnet/commands/dotnet-add-p2p/Program.cs
+++ b/src/dotnet/commands/dotnet-add-p2p/Program.cs
@@ -1,15 +1,9 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-using System.Collections.Generic;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Utils;
-using System;
-using System.IO;
-using System.Linq;
-using Microsoft.Build.Construction;
-using Microsoft.Build.Evaluation;
-using Microsoft.DotNet.Tools.Common;
+using System.Collections.Generic;
namespace Microsoft.DotNet.Tools.Add.ProjectToProjectReference
{
@@ -52,41 +46,27 @@ namespace Microsoft.DotNet.Tools.Add.ProjectToProjectReference
throw new GracefulException(LocalizableStrings.RequiredArgumentNotPassed, "");
}
- ProjectRootElement project;
- string projectDir;
- if (File.Exists(projectArgument.Value))
- {
- project = GetProjectFromFileOrThrow(projectArgument.Value);
- projectDir = new FileInfo(projectArgument.Value).DirectoryName;
- }
- else
- {
- project = GetProjectFromDirectoryOrThrow(projectArgument.Value);
- projectDir = projectArgument.Value;
- }
-
- projectDir = PathUtility.EnsureTrailingSlash(projectDir);
+ var msbuildProj = MsbuildProject.FromFileOrDirectory(projectArgument.Value);
if (app.RemainingArguments.Count == 0)
{
- throw new GracefulException(LocalizableStrings.SpecifyAtLeastOneReference);
+ throw new GracefulException(LocalizableStrings.SpecifyAtLeastOneReferenceToAdd);
}
List references = app.RemainingArguments;
if (!forceOption.HasValue())
{
- EnsureAllReferencesExist(references);
- ConvertPathsToRelative(projectDir, ref references);
+ MsbuildProject.EnsureAllReferencesExist(references);
+ msbuildProj.ConvertPathsToRelative(ref references);
}
- int numberOfAddedReferences = AddProjectToProjectReference(
- project,
+ int numberOfAddedReferences = msbuildProj.AddProjectToProjectReferences(
frameworkOption.Value(),
references);
if (numberOfAddedReferences != 0)
{
- project.Save();
+ msbuildProj.Project.Save();
}
return 0;
@@ -103,133 +83,5 @@ namespace Microsoft.DotNet.Tools.Add.ProjectToProjectReference
return 1;
}
}
-
- internal static void EnsureAllReferencesExist(List references)
- {
- var notExisting = new List();
- foreach (var r in references)
- {
- if (!File.Exists(r))
- {
- notExisting.Add(r);
- }
- }
-
- if (notExisting.Count > 0)
- {
- throw new GracefulException(
- string.Join(
- Environment.NewLine,
- notExisting.Select((r) => string.Format(LocalizableStrings.ReferenceDoesNotExist, r))));
- }
- }
-
- internal static void ConvertPathsToRelative(string root, ref List references)
- {
- root = PathUtility.EnsureTrailingSlash(Path.GetFullPath(root));
- references = references.Select((r) => PathUtility.GetRelativePath(root, Path.GetFullPath(r))).ToList();
- }
-
- // There is ProjectRootElement.TryOpen but it does not work as expected
- // I.e. it returns null for some valid projects
- internal static ProjectRootElement TryOpenProject(string filename)
- {
- try
- {
- return ProjectRootElement.Open(filename, new ProjectCollection(), preserveFormatting: true);
- }
- catch (Microsoft.Build.Exceptions.InvalidProjectFileException)
- {
- return null;
- }
- }
-
- internal static ProjectRootElement GetProjectFromFileOrThrow(string filename)
- {
- if (!File.Exists(filename))
- {
- throw new GracefulException(LocalizableStrings.ProjectDoesNotExist, filename);
- }
-
- var project = TryOpenProject(filename);
- if (project == null)
- {
- throw new GracefulException(LocalizableStrings.ProjectIsInvalid, filename);
- }
-
- return project;
- }
-
- internal static ProjectRootElement GetProjectFromDirectoryOrThrow(string directory)
- {
- DirectoryInfo dir;
- try
- {
- dir = new DirectoryInfo(directory);
- }
- catch (ArgumentException)
- {
- throw new GracefulException(LocalizableStrings.CouldNotFindProjectOrDirectory, directory);
- }
-
- if (!dir.Exists)
- {
- throw new GracefulException(LocalizableStrings.CouldNotFindProjectOrDirectory, directory);
- }
-
- FileInfo[] files = dir.GetFiles("*proj");
- if (files.Length == 0)
- {
- throw new GracefulException(LocalizableStrings.CouldNotFindAnyProjectInDirectory, directory);
- }
-
- if (files.Length > 1)
- {
- throw new GracefulException(LocalizableStrings.MoreThanOneProjectInDirectory, directory);
- }
-
- FileInfo projectFile = files.First();
-
- if (!projectFile.Exists)
- {
- throw new GracefulException(LocalizableStrings.CouldNotFindAnyProjectInDirectory, directory);
- }
-
- var ret = TryOpenProject(projectFile.FullName);
- if (ret == null)
- {
- throw new GracefulException(LocalizableStrings.FoundInvalidProject, projectFile.FullName);
- }
-
- return ret;
- }
-
- private static string NormalizeSlashesForMsbuild(string path)
- {
- return path.Replace('/', '\\');
- }
-
- internal static int AddProjectToProjectReference(ProjectRootElement root, string framework, IEnumerable refs)
- {
- int numberOfAddedReferences = 0;
- const string ProjectItemElementType = "ProjectReference";
-
- ProjectItemGroupElement itemGroup = root.FindUniformOrCreateItemGroupWithCondition(ProjectItemElementType, framework);
- foreach (var @ref in refs.Select((r) => NormalizeSlashesForMsbuild(r)))
- {
- if (root.HasExistingItemWithCondition(framework, @ref))
- {
- Reporter.Output.WriteLine(string.Format(LocalizableStrings.ProjectAlreadyHasAreference, @ref));
- continue;
- }
-
- numberOfAddedReferences++;
- itemGroup.AppendChild(root.CreateItemElement(ProjectItemElementType, @ref));
-
- Reporter.Output.WriteLine(string.Format(LocalizableStrings.ReferenceAddedToTheProject, @ref));
- }
-
- return numberOfAddedReferences;
- }
}
}
diff --git a/src/dotnet/commands/dotnet-add/Program.cs b/src/dotnet/commands/dotnet-add/Program.cs
index 78cb27b6b..d8afe2593 100644
--- a/src/dotnet/commands/dotnet-add/Program.cs
+++ b/src/dotnet/commands/dotnet-add/Program.cs
@@ -14,9 +14,9 @@ using Microsoft.DotNet.Tools.Add.ProjectToProjectReference;
namespace Microsoft.DotNet.Tools.Add
{
- public class AddCommand
+ public class AddCommand : DispatchCommand
{
- public const string HelpText = @".NET Add Command
+ protected override string HelpText => @".NET Add Command
Usage: dotnet add [options]