diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs index 62cc1533ca3..5e1244d29bc 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs @@ -57,7 +57,7 @@ public async Task ExecuteCommand(PackageReferenceArgs packageReferenceArgs, typeConstraint: LibraryDependencyTarget.Package) }; - msBuild.AddPackageReference(packageReferenceArgs.ProjectPath, libraryDependency); + msBuild.AddPackageReference(packageReferenceArgs.ProjectPath, libraryDependency, packageReferenceArgs.NoVersion); return 0; } @@ -219,7 +219,7 @@ public async Task ExecuteCommand(PackageReferenceArgs packageReferenceArgs, // generate a library dependency with all the metadata like Include, Exlude and SuppressParent var libraryDependency = GenerateLibraryDependency(updatedPackageSpec, packageReferenceArgs, restorePreviewResult, userSpecifiedFrameworks, packageDependency); - msBuild.AddPackageReference(packageReferenceArgs.ProjectPath, libraryDependency); + msBuild.AddPackageReference(packageReferenceArgs.ProjectPath, libraryDependency, packageReferenceArgs.NoVersion); } else { @@ -239,7 +239,8 @@ public async Task ExecuteCommand(PackageReferenceArgs packageReferenceArgs, msBuild.AddPackageReferencePerTFM(packageReferenceArgs.ProjectPath, libraryDependency, - compatibleOriginalFrameworks); + compatibleOriginalFrameworks, + packageReferenceArgs.NoVersion); } // 6. Commit restore result @@ -276,6 +277,9 @@ private static LibraryDependency GenerateLibraryDependency( // update default packages path if user specified custom package directory var packagesPath = project.RestoreMetadata.PackagesPath; + // get if the project is onboarded to CPM + var isCentralPackageManagementEnabled = project.RestoreMetadata.CentralPackageVersionsEnabled; + if (!string.IsNullOrEmpty(packageReferenceArgs.PackageDirectory)) { packagesPath = packageReferenceArgs.PackageDirectory; @@ -314,6 +318,7 @@ private static LibraryDependency GenerateLibraryDependency( if (dependency != null) { dependency.LibraryRange.VersionRange = version; + dependency.VersionCentrallyManaged = isCentralPackageManagementEnabled; return dependency; } } @@ -324,7 +329,8 @@ private static LibraryDependency GenerateLibraryDependency( LibraryRange = new LibraryRange( name: packageReferenceArgs.PackageId, versionRange: version, - typeConstraint: LibraryDependencyTarget.Package) + typeConstraint: LibraryDependencyTarget.Package), + VersionCentrallyManaged = isCentralPackageManagementEnabled }; } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 0e3d04b2fd2..682ef6c4b47 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -528,6 +528,15 @@ internal static string Info_AddPkgCompatibleWithSubsetFrameworks { } } + /// + /// Looks up a localized string similar to PackageReference for package '{0}' added to '{1}' and PackageVersion added to central package management file '{2}'.. + /// + internal static string Info_AddPkgCPM { + get { + return ResourceManager.GetString("Info_AddPkgCPM", resourceCulture); + } + } + /// /// Looks up a localized string similar to PackageReference for package '{0}' version '{1}' updated in file '{2}'.. /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 534418ece82..0ca1a7d434e 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -760,5 +760,8 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Invalid culture identifier in {0} environment variable. Value read is '{1}' 0 - Environment variable name, 1 - value set in environment variable + + PackageReference for package '{0}' added to '{1}' and PackageVersion added to central package management file '{2}'. + 0 - The package ID. 1 - Directory.Packages.props file path. 2 - Project file path. \ No newline at end of file diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index b97882dd120..769aa6939b7 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -21,6 +21,7 @@ namespace NuGet.CommandLine.XPlat internal class MSBuildAPIUtility { private const string PACKAGE_REFERENCE_TYPE_TAG = "PackageReference"; + private const string PACKAGE_VERSION_TYPE_TAG = "PackageVersion"; private const string VERSION_TAG = "Version"; private const string FRAMEWORK_TAG = "TargetFramework"; private const string FRAMEWORKS_TAG = "TargetFrameworks"; @@ -32,6 +33,10 @@ internal class MSBuildAPIUtility private const string IncludeAssets = "IncludeAssets"; private const string PrivateAssets = "PrivateAssets"; private const string CollectPackageReferences = "CollectPackageReferences"; + /// + /// The name of the MSBuild property that represents the path to the central package management file, usually Directory.Packages.props. + /// + private const string DirectoryPackagesPropsPathPropertyName = "DirectoryPackagesPropsPath"; public ILogger Logger { get; } @@ -120,7 +125,8 @@ public int RemovePackageReference(string projectPath, LibraryDependency libraryD /// /// Path to the csproj file of the project. /// Package Dependency of the package to be added. - public void AddPackageReference(string projectPath, LibraryDependency libraryDependency) + /// If a version is passed in as a CLI argument. + public void AddPackageReference(string projectPath, LibraryDependency libraryDependency, bool noVersion) { var project = GetProject(projectPath); @@ -128,7 +134,7 @@ public void AddPackageReference(string projectPath, LibraryDependency libraryDep // If the project has a conditional reference, then an unconditional reference is not added. var existingPackageReferences = GetPackageReferencesForAllFrameworks(project, libraryDependency); - AddPackageReference(project, libraryDependency, existingPackageReferences); + AddPackageReference(project, libraryDependency, existingPackageReferences, noVersion); ProjectCollection.GlobalProjectCollection.UnloadProject(project); } @@ -138,8 +144,9 @@ public void AddPackageReference(string projectPath, LibraryDependency libraryDep /// Path to the csproj file of the project. /// Package Dependency of the package to be added. /// Target Frameworks for which the package reference should be added. + /// If a version is passed in as a CLI argument. public void AddPackageReferencePerTFM(string projectPath, LibraryDependency libraryDependency, - IEnumerable frameworks) + IEnumerable frameworks, bool noVersion) { foreach (var framework in frameworks) { @@ -147,33 +154,178 @@ public void AddPackageReferencePerTFM(string projectPath, LibraryDependency libr { { "TargetFramework", framework } }; var project = GetProject(projectPath, globalProperties); var existingPackageReferences = GetPackageReferences(project, libraryDependency); - AddPackageReference(project, libraryDependency, existingPackageReferences, framework); + AddPackageReference(project, libraryDependency, existingPackageReferences, noVersion, framework); ProjectCollection.GlobalProjectCollection.UnloadProject(project); } } + /// + /// Add package version/package reference to the solution/project based on if the project has been onboarded to CPM or not. + /// + /// Project that needs to be modified. + /// Package Dependency of the package to be added. + /// Package references that already exist in the project. + /// If a version is passed in as a CLI argument. + /// Target Framework for which the package reference should be added. private void AddPackageReference(Project project, LibraryDependency libraryDependency, IEnumerable existingPackageReferences, + bool noVersion, string framework = null) { + // Getting all the item groups in a given project var itemGroups = GetItemGroups(project); - if (!existingPackageReferences.Any()) + // Add packageReference to the project file only if it does not exist. + var itemGroup = GetItemGroup(itemGroups, PACKAGE_REFERENCE_TYPE_TAG) ?? CreateItemGroup(project, framework); + + if (!libraryDependency.VersionCentrallyManaged) { - // Add packageReference only if it does not exist. - var itemGroup = GetItemGroup(itemGroups, PACKAGE_REFERENCE_TYPE_TAG) ?? CreateItemGroup(project, framework); - AddPackageReferenceIntoItemGroup(itemGroup, libraryDependency); + if (!existingPackageReferences.Any()) + { + //Modify the project file. + AddPackageReferenceIntoItemGroup(itemGroup, libraryDependency); + } + else + { + // If the package already has a reference then try to update the reference. + UpdatePackageReferenceItems(existingPackageReferences, libraryDependency); + } } else { - // If the package already has a reference then try to update the reference. - UpdatePackageReferenceItems(existingPackageReferences, libraryDependency); + if (!existingPackageReferences.Any()) + { + //Add to the project file. + AddPackageReferenceIntoItemGroupCPM(project, itemGroup, libraryDependency); + } + + // Get package version if it already exists in the props file. Returns null if there is no matching package version. + ProjectItem packageVersionInProps = project.Items.LastOrDefault(i => i.ItemType == PACKAGE_VERSION_TYPE_TAG && i.EvaluatedInclude.Equals(libraryDependency.Name)); + + // If no exists in the Directory.Packages.props file. + if (packageVersionInProps == null) + { + // Modifying the props file if project is onboarded to CPM. + AddPackageVersionIntoItemGroupCPM(project, libraryDependency); + } + else + { + // Modify the Directory.Packages.props file with the version that is passed in. + if (!noVersion) + { + string packageVersion = libraryDependency.LibraryRange.VersionRange.OriginalString; + UpdatePackageVersion(project, packageVersionInProps, packageVersion); + } + } } + project.Save(); } - private static IEnumerable GetItemGroups(Project project) + /// + /// Add package name and version using PackageVersion tag for projects onboarded to CPM. + /// + /// Project that needs to be modified. + /// Package Dependency of the package to be added. + private void AddPackageVersionIntoItemGroupCPM(Project project, LibraryDependency libraryDependency) + { + // If onboarded to CPM get the directoryBuildPropsRootElement. + ProjectRootElement directoryBuildPropsRootElement = GetDirectoryBuildPropsRootElement(project); + + // Get the ItemGroup to add a PackageVersion to or create a new one. + var propsItemGroup = GetItemGroup(directoryBuildPropsRootElement.ItemGroups, PACKAGE_VERSION_TYPE_TAG) ?? directoryBuildPropsRootElement.AddItemGroup(); + AddPackageVersionIntoPropsItemGroup(propsItemGroup, libraryDependency); + + // Save the updated props file. + directoryBuildPropsRootElement.Save(); + } + + /// + /// Get the Directory build props root element for projects onboarded to CPM. + /// + /// Project that needs to be modified. + /// The directory build props root element. + internal ProjectRootElement GetDirectoryBuildPropsRootElement(Project project) + { + // Get the Directory.Packages.props path. + string directoryPackagesPropsPath = project.GetPropertyValue(DirectoryPackagesPropsPathPropertyName); + ProjectRootElement directoryBuildPropsRootElement = project.Imports.FirstOrDefault(i => i.ImportedProject.FullPath.Equals(directoryPackagesPropsPath)).ImportedProject; + return directoryBuildPropsRootElement; + } + + /// + /// Add package name and version into the props file. + /// + /// Item group that needs to be modified in the props file. + /// Package Dependency of the package to be added. + internal void AddPackageVersionIntoPropsItemGroup(ProjectItemGroupElement itemGroup, + LibraryDependency libraryDependency) + { + // Add both package reference information and version metadata using the PACKAGE_VERSION_TYPE_TAG. + var item = itemGroup.AddItem(PACKAGE_VERSION_TYPE_TAG, libraryDependency.Name); + var packageVersion = AddVersionMetadata(libraryDependency, item); + AddExtraMetadataToProjectItemElement(libraryDependency, item); + Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgAdded, libraryDependency.Name, packageVersion, itemGroup.ContainingProject.FullPath + )); + } + + /// + /// Add package name and version into item group. + /// + /// Item group to add to. + /// Package Dependency of the package to be added. + private void AddPackageReferenceIntoItemGroup(ProjectItemGroupElement itemGroup, + LibraryDependency libraryDependency) + { + // Add both package reference information and version metadata using the PACKAGE_REFERENCE_TYPE_TAG. + var item = itemGroup.AddItem(PACKAGE_REFERENCE_TYPE_TAG, libraryDependency.Name); + var packageVersion = AddVersionMetadata(libraryDependency, item); + AddExtraMetadataToProjectItemElement(libraryDependency, item); + Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgAdded, libraryDependency.Name, packageVersion, itemGroup.ContainingProject.FullPath)); + } + + /// + /// Add only the package name into the project file for projects onboarded to CPM. + /// + /// Project to be modified. + /// Item group to add to. + /// Package Dependency of the package to be added. + internal void AddPackageReferenceIntoItemGroupCPM(Project project, ProjectItemGroupElement itemGroup, + LibraryDependency libraryDependency) + { + // Only add the package reference information using the PACKAGE_REFERENCE_TYPE_TAG. + ProjectItemElement item = itemGroup.AddItem(PACKAGE_REFERENCE_TYPE_TAG, libraryDependency.Name); + AddExtraMetadataToProjectItemElement(libraryDependency, item); + Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgCPM, libraryDependency.Name, project.GetPropertyValue(DirectoryPackagesPropsPathPropertyName), itemGroup.ContainingProject.FullPath)); + } + + /// + /// Add other metadata based on certain flags. + /// + /// Package Dependency of the package to be added. + /// Item to add the metadata to. + private void AddExtraMetadataToProjectItemElement(LibraryDependency libraryDependency, ProjectItemElement item) + { + if (libraryDependency.IncludeType != LibraryIncludeFlags.All) + { + var includeFlags = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.IncludeType)); + item.AddMetadata(IncludeAssets, includeFlags, expressAsAttribute: false); + } + + if (libraryDependency.SuppressParent != LibraryIncludeFlagUtils.DefaultSuppressParent) + { + var suppressParent = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.SuppressParent)); + item.AddMetadata(PrivateAssets, suppressParent, expressAsAttribute: false); + } + } + + /// + /// Get all the item groups in a given project. + /// + /// A specified project. + /// + internal IEnumerable GetItemGroups(Project project) { return project .Items @@ -188,7 +340,7 @@ private static IEnumerable GetItemGroups(Project projec /// List of all item groups in the project /// An item type tag that must be in the item group. It if PackageReference in this case. /// An ItemGroup, which could be null. - private static ProjectItemGroupElement GetItemGroup(IEnumerable itemGroups, + internal ProjectItemGroupElement GetItemGroup(IEnumerable itemGroups, string itemType) { var itemGroup = itemGroups? @@ -198,7 +350,13 @@ private static ProjectItemGroupElement GetItemGroup(IEnumerable + /// Creating an item group in a project. + /// + /// Project where the item group should be created. + /// Target Framework for which the package reference should be added. + /// An Item Group. + internal ProjectItemGroupElement CreateItemGroup(Project project, string framework = null) { // Create a new item group and add a condition if given var itemGroup = project.Xml.AddItemGroup(); @@ -209,8 +367,39 @@ private static ProjectItemGroupElement CreateItemGroup(Project project, string f return itemGroup; } + /// + /// Adding version metadata to a given project item element. + /// + /// Package Dependency of the package to be added. + /// The item that the version metadata should be added to. + /// The package version that is added in the metadata. + private string AddVersionMetadata(LibraryDependency libraryDependency, ProjectItemElement item) + { + var packageVersion = libraryDependency.LibraryRange.VersionRange.OriginalString ?? + libraryDependency.LibraryRange.VersionRange.MinVersion.ToString(); + + ProjectMetadataElement versionAttribute = item.Metadata.FirstOrDefault(i => i.Name.Equals("Version")); + + // If version attribute does not exist at all, add it. + if (versionAttribute == null) + { + item.AddMetadata(VERSION_TAG, packageVersion, expressAsAttribute: true); + } + // Else, just update the version in the already existing version attribute. + else + { + versionAttribute.Value = packageVersion; + } + return packageVersion; + } + + /// + /// Update package references for a project that is not onboarded to CPM. + /// + /// Existing package references. + /// Package Dependency of the package to be added. private void UpdatePackageReferenceItems(IEnumerable packageReferencesItems, - LibraryDependency libraryDependency) + LibraryDependency libraryDependency) { // We validate that the operation does not update any imported items // If it does then we throw a user friendly exception without making any changes @@ -223,17 +412,7 @@ private void UpdatePackageReferenceItems(IEnumerable packageReferen packageReferenceItem.SetMetadataValue(VERSION_TAG, packageVersion); - if (libraryDependency.IncludeType != LibraryIncludeFlags.All) - { - var includeFlags = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.IncludeType)); - packageReferenceItem.SetMetadataValue(IncludeAssets, includeFlags); - } - - if (libraryDependency.SuppressParent != LibraryIncludeFlagUtils.DefaultSuppressParent) - { - var suppressParent = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.SuppressParent)); - packageReferenceItem.SetMetadataValue(PrivateAssets, suppressParent); - } + UpdateExtraMetadataInProjectItem(libraryDependency, packageReferenceItem); Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgUpdated, @@ -243,34 +422,31 @@ private void UpdatePackageReferenceItems(IEnumerable packageReferen } } - private void AddPackageReferenceIntoItemGroup(ProjectItemGroupElement itemGroup, - LibraryDependency libraryDependency) + /// + /// Update the element if a version is passed in as a CLI argument. + /// + /// + /// item with a matching package ID. + /// Version that is passed in as a CLI argument. + internal void UpdatePackageVersion(Project project, ProjectItem packageVersion, string versionCLIArgument) { - var packageVersion = libraryDependency.LibraryRange.VersionRange.OriginalString ?? - libraryDependency.LibraryRange.VersionRange.MinVersion.ToString(); - - var item = itemGroup.AddItem(PACKAGE_REFERENCE_TYPE_TAG, libraryDependency.Name); - item.AddMetadata(VERSION_TAG, packageVersion, expressAsAttribute: true); - - if (libraryDependency.IncludeType != LibraryIncludeFlags.All) - { - var includeFlags = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.IncludeType)); - item.AddMetadata(IncludeAssets, includeFlags, expressAsAttribute: false); - } - - if (libraryDependency.SuppressParent != LibraryIncludeFlagUtils.DefaultSuppressParent) - { - var suppressParent = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.SuppressParent)); - item.AddMetadata(PrivateAssets, suppressParent, expressAsAttribute: false); - } - - Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, - Strings.Info_AddPkgAdded, - libraryDependency.Name, - packageVersion, - itemGroup.ContainingProject.FullPath)); + // Determine where the item is decalred + ProjectItemElement packageVersionItemElement = project.GetItemProvenance(packageVersion).LastOrDefault()?.ItemElement; + + // Get the Version attribute on the packageVersionItemElement. + ProjectMetadataElement versionAttribute = packageVersionItemElement.Metadata.FirstOrDefault(i => i.Name.Equals("Version")); + // Update the version + versionAttribute.Value = versionCLIArgument; + packageVersionItemElement.ContainingProject.Save(); } + /// + /// Validate that no imported items in the project are updated with the package version. + /// + /// Existing package reference items. + /// Package Dependency of the package to be added. + /// Operation types such as if a package reference is being updated. + /// private static void ValidateNoImportedItemsAreUpdated(IEnumerable packageReferencesItems, LibraryDependency libraryDependency, string operationType) @@ -300,6 +476,46 @@ private static void ValidateNoImportedItemsAreUpdated(IEnumerable p } } + /// + /// Update other metadata for items based on certain flags. + /// + /// Package Dependency of the package to be added. + /// Item to be modified. + private void UpdateExtraMetadataInProjectItem(LibraryDependency libraryDependency, ProjectItem packageReferenceItem) + { + if (libraryDependency.IncludeType != LibraryIncludeFlags.All) + { + var includeFlags = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.IncludeType)); + packageReferenceItem.SetMetadataValue(IncludeAssets, includeFlags); + } + + if (libraryDependency.SuppressParent != LibraryIncludeFlagUtils.DefaultSuppressParent) + { + var suppressParent = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.SuppressParent)); + packageReferenceItem.SetMetadataValue(PrivateAssets, suppressParent); + } + } + + /// + /// Update other metadata for items based on certain flags. + /// + /// Package Dependency of the package to be added. + /// Item to be modified. + private void UpdateExtraMetadata(LibraryDependency libraryDependency, ProjectItem packageReferenceItem) + { + if (libraryDependency.IncludeType != LibraryIncludeFlags.All) + { + var includeFlags = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.IncludeType)); + packageReferenceItem.SetMetadataValue(IncludeAssets, includeFlags); + } + + if (libraryDependency.SuppressParent != LibraryIncludeFlagUtils.DefaultSuppressParent) + { + var suppressParent = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.SuppressParent)); + packageReferenceItem.SetMetadataValue(PrivateAssets, suppressParent); + } + } + /// /// A simple check for some of the evaluated properties to check /// if the project is package reference project or not diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs index 42aeb037f91..61ae406508f 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs @@ -684,5 +684,262 @@ private void CopyResourceToDirectory(string resourceName, DirectoryInfo director stream.CopyToFile(destinationFilePath); } } + + public async Task AddPkg_WithCPM_WhenPackageVersionDoesNotExistAndVersionCLIArgNotPassed_Success() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution, and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net5.0"); + + const string version1 = "1.0.0"; + const string version2 = "2.0.0"; + const string packageX = "X"; + + var packageFrameworks = "net5.0"; + var packageX100 = XPlatTestUtils.CreatePackage(packageX, version1, frameworkString: packageFrameworks); + var packageX200 = XPlatTestUtils.CreatePackage(packageX, version2, frameworkString: packageFrameworks); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX100, + packageX200); + + var propsFile = @$" + + true + + + "; + + solution.Projects.Add(projectA); + solution.Create(pathContext.SolutionRoot); + + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"), propsFile); + + var projectADirectory = Path.Combine(pathContext.SolutionRoot, projectA.ProjectName); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX}", ignoreExitCode: true); + + // Assert + Assert.True(result.Success, result.Output); + Assert.Contains(@$" + + + + true + + + "; + + solution.Projects.Add(projectA); + solution.Create(pathContext.SolutionRoot); + + + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"), propsFile); + var projectADirectory = Path.Combine(pathContext.SolutionRoot, projectA.ProjectName); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version1}", ignoreExitCode: true); + + // Assert + Assert.True(result.Success, result.Output); + Assert.Contains(@$" + + + + true + + + + + + "; + + solution.Projects.Add(projectA); + solution.Create(pathContext.SolutionRoot); + + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"), propsFile); + var projectADirectory = Path.Combine(pathContext.SolutionRoot, projectA.ProjectName); + + //Act + //By default the package version used will be 2.0.0 since no version CLI argument is passed in the CLI command. + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX}", ignoreExitCode: true); + + // Assert + Assert.True(result.Success, result.Output); + // Checking that the PackageVersion is not updated. + Assert.Contains(@$"", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + + var projectFileFromDisk = File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj")); + + // Checking that version metadata is not added to the project files. + Assert.Contains(@$"Include=""X""", projectFileFromDisk); + Assert.DoesNotContain(@$"Include=""X"" Version=""1.0.0""", projectFileFromDisk); + } + + [Fact] + public async Task AddPkg_WithCPM_WhenPackageVersionExistsAndVersionCLIArgPassed_Success() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net5.0"); + + const string version = "2.0.0"; + const string packageX = "X"; + + var packageFrameworks = "net5.0"; + var packageX200 = XPlatTestUtils.CreatePackage(packageX, version, frameworkString: packageFrameworks); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX200); + + var propsFile = @$" + + true + + + + + + "; + + solution.Projects.Add(projectA); + solution.Create(pathContext.SolutionRoot); + + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"), propsFile); + var projectADirectory = Path.Combine(pathContext.SolutionRoot, projectA.ProjectName); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version}", ignoreExitCode: true); + + // Assert + Assert.True(result.Success, result.Output); + Assert.Contains(@$"", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + + var projectFileFromDisk = File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj")); + + // Checking that version metadata is not added to the project files. + Assert.Contains(@$"Include=""X""", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + Assert.DoesNotContain(@$"Include=""X"" Version=""1.0.0""", projectFileFromDisk); + Assert.DoesNotContain(@$"Include=""X"" Version=""2.0.0""", projectFileFromDisk); + } + + [Fact] + public async Task AddPkg_WithCPM_WhenMultipleItemGroupsExist_Success() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net5.0"); + + const string version = "1.0.0"; + const string packageX = "X"; + + var packageFrameworks = "net5.0"; + var packageX100 = XPlatTestUtils.CreatePackage(packageX, version, frameworkString: packageFrameworks); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX100); + + var propsFile = @$" + + true + + + + + + + +"; + + solution.Projects.Add(projectA); + solution.Create(pathContext.SolutionRoot); + + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"), propsFile); + var projectADirectory = Path.Combine(pathContext.SolutionRoot, projectA.ProjectName); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version}", ignoreExitCode: true); + + // Assert + Assert.True(result.Success, result.Output); + + var propsFileFromDisk = File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props")); + + Assert.Contains(@$" + + + ", propsFileFromDisk); + + Assert.DoesNotContain($@"< ItemGroup > + < Content Include = ""SomeFile"" /> + + ", propsFileFromDisk); + } } } diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs new file mode 100644 index 00000000000..2ef55850e85 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs @@ -0,0 +1,388 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Linq; +using Microsoft.Build.Definition; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Locator; +using NuGet.CommandLine.XPlat; +using NuGet.LibraryModel; +using NuGet.Test.Utility; +using NuGet.Versioning; +using Xunit; +using Project = Microsoft.Build.Evaluation.Project; + +namespace NuGet.CommandLine.Xplat.Tests +{ + public class MSBuildAPIUtilityTests + { + static MSBuildAPIUtilityTests() + { + MSBuildLocator.RegisterDefaults(); + } + + [PlatformFact(Platform.Windows)] + public void GetDirectoryBuildPropsRootElementWhenItExists_Success() + { + // Arrange + var testDirectory = TestDirectory.Create(); + + var projectCollection = new ProjectCollection( + globalProperties: null, + remoteLoggers: null, + loggers: null, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default, + // Having more than 1 node spins up multiple msbuild.exe instances to run builds in parallel + // However, these targets complete so quickly that the added overhead makes it take longer + maxNodeCount: 1, + onlyLogCriticalEvents: false, + loadProjectsReadOnly: false); + + var projectOptions = new ProjectOptions + { + LoadSettings = ProjectLoadSettings.DoNotEvaluateElementsWithFalseCondition, + ProjectCollection = projectCollection + }; + + var propsFile = +@$" + + true + +"; + File.WriteAllText(Path.Combine(testDirectory, "Directory.Packages.props"), propsFile); + + string projectContent = +@$" + + net6.0 + +"; + File.WriteAllText(Path.Combine(testDirectory, "projectA.csproj"), projectContent); + var project = Project.FromFile(Path.Combine(testDirectory, "projectA.csproj"), projectOptions); + + // Act + var result = new MSBuildAPIUtility(logger: new TestLogger()).GetDirectoryBuildPropsRootElement(project); + + // Assert + Assert.Equal(Path.Combine(testDirectory, "Directory.Packages.props"), result.FullPath); + } + + [PlatformFact(Platform.Windows)] + public void AddPackageReferenceIntoProjectFileWhenItemGroupDoesNotExist_Success() + { + // Arrange + var testDirectory = TestDirectory.Create(); + var projectCollection = new ProjectCollection( + globalProperties: null, + remoteLoggers: null, + loggers: null, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default, + // Having more than 1 node spins up multiple msbuild.exe instances to run builds in parallel + // However, these targets complete so quickly that the added overhead makes it take longer + maxNodeCount: 1, + onlyLogCriticalEvents: false, + loadProjectsReadOnly: false); + + var projectOptions = new ProjectOptions + { + LoadSettings = ProjectLoadSettings.DoNotEvaluateElementsWithFalseCondition, + ProjectCollection = projectCollection + }; + + // Arrange project file + string projectContent = +@$" + +net6.0 + +"; + File.WriteAllText(Path.Combine(testDirectory, "projectA.csproj"), projectContent); + var project = Project.FromFile(Path.Combine(testDirectory, "projectA.csproj"), projectOptions); + + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + // Creating an item group in the project + var itemGroup = msObject.CreateItemGroup(project, null); + + var libraryDependency = new LibraryDependency + { + LibraryRange = new LibraryRange( + name: "X", + versionRange: VersionRange.Parse("1.0.0"), + typeConstraint: LibraryDependencyTarget.Package) + }; + + // Act + msObject.AddPackageReferenceIntoItemGroupCPM(project, itemGroup, libraryDependency); + project.Save(); + + // Assert + string updatedProjectFile = File.ReadAllText(Path.Combine(testDirectory, "projectA.csproj")); + Assert.Contains(@$"", updatedProjectFile); + Assert.DoesNotContain(@$"", updatedProjectFile); + } + + [PlatformFact(Platform.Windows)] + public void AddPackageReferenceIntoProjectFileWhenItemGroupDoesExist_Success() + { + // Arrange + var testDirectory = TestDirectory.Create(); + var projectCollection = new ProjectCollection( + globalProperties: null, + remoteLoggers: null, + loggers: null, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default, + // Having more than 1 node spins up multiple msbuild.exe instances to run builds in parallel + // However, these targets complete so quickly that the added overhead makes it take longer + maxNodeCount: 1, + onlyLogCriticalEvents: false, + loadProjectsReadOnly: false); + + var projectOptions = new ProjectOptions + { + LoadSettings = ProjectLoadSettings.DoNotEvaluateElementsWithFalseCondition, + ProjectCollection = projectCollection + }; + + // Arrange project file + string projectContent = +@$" + +net6.0 + + + + +"; + File.WriteAllText(Path.Combine(testDirectory, "projectA.csproj"), projectContent); + var project = Project.FromFile(Path.Combine(testDirectory, "projectA.csproj"), projectOptions); + + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + // Getting all the item groups in a given project + var itemGroups = msObject.GetItemGroups(project); + // Getting an existing item group that has package reference(s) + var itemGroup = msObject.GetItemGroup(itemGroups, "PackageReference"); + + var libraryDependency = new LibraryDependency + { + LibraryRange = new LibraryRange( + name: "X", + versionRange: VersionRange.Parse("1.0.0"), + typeConstraint: LibraryDependencyTarget.Package) + }; + + // Act + msObject.AddPackageReferenceIntoItemGroupCPM(project, itemGroup, libraryDependency); + project.Save(); + + // Assert + string updatedProjectFile = File.ReadAllText(Path.Combine(testDirectory, "projectA.csproj")); + Assert.Contains(@$"", updatedProjectFile); + Assert.DoesNotContain(@$"", updatedProjectFile); + } + + [PlatformFact(Platform.Windows)] + public void AddPackageVersionIntoPropsFileWhenItemGroupDoesNotExist_Success() + { + // Arrange + var testDirectory = TestDirectory.Create(); + var projectCollection = new ProjectCollection( + globalProperties: null, + remoteLoggers: null, + loggers: null, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default, + // Having more than 1 node spins up multiple msbuild.exe instances to run builds in parallel + // However, these targets complete so quickly that the added overhead makes it take longer + maxNodeCount: 1, + onlyLogCriticalEvents: false, + loadProjectsReadOnly: false); + + var projectOptions = new ProjectOptions + { + LoadSettings = ProjectLoadSettings.DoNotEvaluateElementsWithFalseCondition, + ProjectCollection = projectCollection + }; + + // Arrange Directory.Packages.props file + var propsFile = +@$" + + true + +"; + File.WriteAllText(Path.Combine(testDirectory, "Directory.Packages.props"), propsFile); + + // Arrange project file + string projectContent = +@$" + + net6.0 + +"; + File.WriteAllText(Path.Combine(testDirectory, "projectA.csproj"), projectContent); + var project = Project.FromFile(Path.Combine(testDirectory, "projectA.csproj"), projectOptions); + + // Add item group to Directory.Packages.props + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + var directoryBuildPropsRootElement = msObject.GetDirectoryBuildPropsRootElement(project); + var propsItemGroup = directoryBuildPropsRootElement.AddItemGroup(); + + var libraryDependency = new LibraryDependency + { + LibraryRange = new LibraryRange( + name: "X", + versionRange: VersionRange.Parse("1.0.0"), + typeConstraint: LibraryDependencyTarget.Package) + }; + + // Act + msObject.AddPackageVersionIntoPropsItemGroup(propsItemGroup, libraryDependency); + // Save the updated props file. + directoryBuildPropsRootElement.Save(); + + // Assert + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(testDirectory, "Directory.Packages.props"))); + } + + [PlatformFact(Platform.Windows)] + public void AddPackageVersionIntoPropsFileWhenItemGroupExists_Success() + { + // Arrange + var testDirectory = TestDirectory.Create(); + var projectCollection = new ProjectCollection( + globalProperties: null, + remoteLoggers: null, + loggers: null, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default, + // Having more than 1 node spins up multiple msbuild.exe instances to run builds in parallel + // However, these targets complete so quickly that the added overhead makes it take longer + maxNodeCount: 1, + onlyLogCriticalEvents: false, + loadProjectsReadOnly: false); + + var projectOptions = new ProjectOptions + { + LoadSettings = ProjectLoadSettings.DoNotEvaluateElementsWithFalseCondition, + ProjectCollection = projectCollection + }; + + // Arrange Directory.Packages.props file + var propsFile = +@$" + + true + + + + +"; + File.WriteAllText(Path.Combine(testDirectory, "Directory.Packages.props"), propsFile); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(testDirectory, "projectA.csproj"), projectContent); + var project = Project.FromFile(Path.Combine(testDirectory, "projectA.csproj"), projectOptions); + + // Get existing item group from Directory.Packages.props + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + var directoryBuildPropsRootElement = msObject.GetDirectoryBuildPropsRootElement(project); + var propsItemGroup = msObject.GetItemGroup(directoryBuildPropsRootElement.ItemGroups, "PackageVersion"); + + var libraryDependency = new LibraryDependency + { + LibraryRange = new LibraryRange( + name: "Y", + versionRange: VersionRange.Parse("1.0.0"), + typeConstraint: LibraryDependencyTarget.Package) + }; + + // Act + msObject.AddPackageVersionIntoPropsItemGroup(propsItemGroup, libraryDependency); + // Save the updated props file + directoryBuildPropsRootElement.Save(); + + // Assert + Assert.Contains(@$"", File.ReadAllText(Path.Combine(testDirectory, "Directory.Packages.props"))); + } + + [PlatformFact(Platform.Windows)] + public void UpdatePackageVersionInPropsFileWhenItExists_Success() + { + // Arrange + var testDirectory = TestDirectory.Create(); + var projectCollection = new ProjectCollection( + globalProperties: null, + remoteLoggers: null, + loggers: null, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default, + // Having more than 1 node spins up multiple msbuild.exe instances to run builds in parallel + // However, these targets complete so quickly that the added overhead makes it take longer + maxNodeCount: 1, + onlyLogCriticalEvents: false, + loadProjectsReadOnly: false); + + var projectOptions = new ProjectOptions + { + LoadSettings = ProjectLoadSettings.DoNotEvaluateElementsWithFalseCondition, + ProjectCollection = projectCollection + }; + + // Arrange Directory.Packages.props file + var propsFile = +@$" + + true + + + + +"; + File.WriteAllText(Path.Combine(testDirectory, "Directory.Packages.props"), propsFile); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(testDirectory, "projectA.csproj"), projectContent); + var project = Project.FromFile(Path.Combine(testDirectory, "projectA.csproj"), projectOptions); + + var msObject = new MSBuildAPIUtility(logger: new TestLogger()); + // Get package version if it already exists in the props file. Returns null if there is no matching package version. + ProjectItem packageVersionInProps = project.Items.LastOrDefault(i => i.ItemType == "PackageVersion" && i.EvaluatedInclude.Equals("X")); + + var libraryDependency = new LibraryDependency + { + LibraryRange = new LibraryRange( + name: "X", + versionRange: VersionRange.Parse("2.0.0"), + typeConstraint: LibraryDependencyTarget.Package) + }; + + // Act + msObject.UpdatePackageVersion(project, packageVersionInProps, "2.0.0"); + + // Assert + Assert.Equal(projectContent, File.ReadAllText(Path.Combine(testDirectory, "projectA.csproj"))); + string updatedPropsFile = File.ReadAllText(Path.Combine(testDirectory, "Directory.Packages.props")); + Assert.Contains(@$"", updatedPropsFile); + Assert.DoesNotContain(@$"", updatedPropsFile); + } + } +} diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj index f0c97435a88..0d12db7b139 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj @@ -17,6 +17,12 @@ + + + + + +