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 5e1244d29bc..8ba60ab4194 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommandRunner.cs @@ -102,6 +102,12 @@ public async Task ExecuteCommand(PackageReferenceArgs packageReferenceArgs, var originalPackageSpec = matchingPackageSpecs.FirstOrDefault(); + // Check if the project files are correct for CPM + if (originalPackageSpec.RestoreMetadata.CentralPackageVersionsEnabled && !msBuild.AreCentralVersionRequirementsSatisfied(packageReferenceArgs, originalPackageSpec)) + { + return 1; + } + // 2. Determine the version // Setup the Credential Service before making any potential http calls. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 4fd5508e54f..5f1fc789bcd 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -330,6 +330,33 @@ internal static string Err_InvalidValue { } } + /// + /// Looks up a localized string similar to VersionOverride for package '{0}' should not be empty.. + /// + internal static string Error_AddPkg_CentralPackageVersions_EmptyVersionOverride { + get { + return ResourceManager.GetString("Error_AddPkg_CentralPackageVersions_EmptyVersionOverride", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Package reference for package '{0}' defined in incorrect location, PackageReference should be defined in project file.. + /// + internal static string Error_AddPkg_CentralPackageVersions_PackageReference_WrongLocation { + get { + return ResourceManager.GetString("Error_AddPkg_CentralPackageVersions_PackageReference_WrongLocation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PackageVersion for package '{0}' defined in incorrect location, PackageVersion should be defined in Directory.Package.props.. + /// + internal static string Error_AddPkg_CentralPackageVersions_PackageVersion_WrongLocation { + get { + return ResourceManager.GetString("Error_AddPkg_CentralPackageVersions_PackageVersion_WrongLocation", resourceCulture); + } + } + /// /// Looks up a localized string similar to Item '{0}' for '{1}' in Imported file '{2}'.. /// @@ -374,6 +401,51 @@ internal static string Error_InvalidCultureInfo { return ResourceManager.GetString("Error_InvalidCultureInfo", resourceCulture); } } + + /// + /// Looks up a localized string similar to The packages {0} are implicitly referenced. You do not typically need to reference them from your project or in your central package versions management file. For more information, see https://aka.ms/sdkimplicitrefs. + /// + internal static string Error_CentralPackageVersions_AutoreferencedReferencesNotAllowed { + get { + return ResourceManager.GetString("Error_CentralPackageVersions_AutoreferencedReferencesNotAllowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Centrally defined floating package versions are not allowed.. + /// + internal static string Error_CentralPackageVersions_FloatingVersionsAreNotAllowed { + get { + return ResourceManager.GetString("Error_CentralPackageVersions_FloatingVersionsAreNotAllowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The PackageReference items {0} do not have corresponding PackageVersion.. + /// + internal static string Error_CentralPackageVersions_MissingPackageVersion { + get { + return ResourceManager.GetString("Error_CentralPackageVersions_MissingPackageVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The package reference {0} specifies a VersionOverride but the ability to override a centrally defined version is currently disabled.. + /// + internal static string Error_CentralPackageVersions_VersionOverrideDisabled { + get { + return ResourceManager.GetString("Error_CentralPackageVersions_VersionOverrideDisabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items: {0}. + /// + internal static string Error_CentralPackageVersions_VersionsNotAllowed { + get { + return ResourceManager.GetString("Error_CentralPackageVersions_VersionsNotAllowed", resourceCulture); + } + } /// /// Looks up a localized string similar to MsBuild was unable to open Project '{0}'.. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 57ce2f0b612..ab94427ff28 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -783,4 +783,33 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT '--output-version' option not applicable for console output, it can only be used together with `--format json` option. Don't localize '--output-version' and `--format json` - \ No newline at end of file + + Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items: {0} + 0 - package id + + + VersionOverride for package '{0}' should not be empty. + 0 - package id + + + Package reference for package '{0}' defined in incorrect location, PackageReference should be defined in project file. + 0 - package id + + + PackageVersion for package '{0}' defined in incorrect location, PackageVersion should be defined in Directory.Package.props. + 0 - package id + + + The packages {0} are implicitly referenced. You do not typically need to reference them from your project or in your central package versions management file. For more information, see https://aka.ms/sdkimplicitrefs + + + Centrally defined floating package versions are not allowed. + + + The PackageReference items {0} do not have corresponding PackageVersion. + + + The package reference {0} specifies a VersionOverride but the ability to override a centrally defined version is currently disabled. + 0 - packagereference name + + diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index 0494f1ea780..aaef4a08a76 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -120,6 +120,87 @@ public int RemovePackageReference(string projectPath, LibraryDependency libraryD } } + /// + /// Check if the project files format are correct for CPM + /// + /// Arguments used in the command + /// + /// + public bool AreCentralVersionRequirementsSatisfied(PackageReferenceArgs packageReferenceArgs, PackageSpec packageSpec) + { + var project = GetProject(packageReferenceArgs.ProjectPath); + string directoryPackagesPropsPath = project.GetPropertyValue(DirectoryPackagesPropsPathPropertyName); + + // Get VersionOverride if it exisits in the package reference. + IEnumerable dependenciesWithVersionOverride = null; + + if (packageSpec.RestoreMetadata.CentralPackageVersionOverrideDisabled) + { + dependenciesWithVersionOverride = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.Dependencies.Where(d => !d.AutoReferenced && d.VersionOverride != null)); + // Emit a error if VersionOverride was specified for a package reference but that functionality is disabled + foreach (var item in dependenciesWithVersionOverride) + { + packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_VersionOverrideDisabled, string.Join(";", dependenciesWithVersionOverride.Select(d => d.Name)))); + return false; + } + } + + // The dependencies should not have versions explicitly defined if cpvm is enabled. + IEnumerable dependenciesWithDefinedVersion = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.Dependencies.Where(d => !d.VersionCentrallyManaged && !d.AutoReferenced && d.VersionOverride == null)); + if (dependenciesWithDefinedVersion.Any()) + { + packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_VersionsNotAllowed, string.Join(";", dependenciesWithDefinedVersion.Select(d => d.Name)))); + return false; + } + IEnumerable autoReferencedAndDefinedInCentralFile = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.Dependencies.Where(d => d.AutoReferenced && tfm.CentralPackageVersions.ContainsKey(d.Name))); + if (autoReferencedAndDefinedInCentralFile.Any()) + { + packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_AutoreferencedReferencesNotAllowed, string.Join(";", autoReferencedAndDefinedInCentralFile.Select(d => d.Name)))); + return false; + } + IEnumerable packageReferencedDependenciesWithoutCentralVersionDefined = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.Dependencies.Where(d => d.LibraryRange.VersionRange == null)); + if (packageReferencedDependenciesWithoutCentralVersionDefined.Any()) + { + packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_MissingPackageVersion, string.Join(";", packageReferencedDependenciesWithoutCentralVersionDefined.Select(d => d.Name)))); + return false; + } + var floatingVersionDependencies = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.CentralPackageVersions.Values).Where(cpv => cpv.VersionRange.IsFloating); + if (floatingVersionDependencies.Any()) + { + packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_FloatingVersionsAreNotAllowed)); + return false; + } + + // PackageVersion should not be defined outside the project file. + var packageVersions = project.Items.Where(item => item.ItemType == PACKAGE_VERSION_TYPE_TAG && item.EvaluatedInclude.Equals(packageReferenceArgs.PackageId) && !item.Xml.ContainingProject.FullPath.Equals(directoryPackagesPropsPath)); + if (packageVersions.Any()) + { + packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_AddPkg_CentralPackageVersions_PackageVersion_WrongLocation, packageReferenceArgs.PackageId)); + return false; + } + + // PackageReference should not be defined in Directory.Packages.props + var packageReferenceOutsideProjectFile = project.Items.Where(item => item.ItemType == PACKAGE_REFERENCE_TYPE_TAG && item.Xml.ContainingProject.FullPath.Equals(directoryPackagesPropsPath)); + if (packageReferenceOutsideProjectFile.Any()) + { + packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_AddPkg_CentralPackageVersions_PackageReference_WrongLocation, packageReferenceArgs.PackageId)); + return false; + } + + ProjectItem packageReference = project.Items.Where(item => item.ItemType == PACKAGE_REFERENCE_TYPE_TAG && item.EvaluatedInclude.Equals(packageReferenceArgs.PackageId)).LastOrDefault(); + ProjectItem packageVersionInProps = packageVersions.LastOrDefault(); + var versionOverride = dependenciesWithVersionOverride?.FirstOrDefault(d => d.Name.Equals(packageReferenceArgs.PackageId)); + + // If package reference exists and the user defined a VersionOverride or PackageVersions but didn't specified a version, no-op + if (packageReference != null && (versionOverride != null || packageVersionInProps != null) && packageReferenceArgs.NoVersion) + { + return false; + } + + ProjectCollection.GlobalProjectCollection.UnloadProject(project); + return true; + } + /// /// Add an unconditional package reference to the project. /// @@ -194,28 +275,42 @@ private void AddPackageReference(Project project, } else { + // Get package version and VersionOverride if it already exists in the props file. Returns null if there is no matching package version. + ProjectItem packageReferenceInProps = project.Items.LastOrDefault(i => i.ItemType == PACKAGE_REFERENCE_TYPE_TAG && i.EvaluatedInclude.Equals(libraryDependency.Name)); + var versionOverrideExists = packageReferenceInProps?.Metadata.FirstOrDefault(i => i.Name.Equals("VersionOverride") && !string.IsNullOrWhiteSpace(i.EvaluatedValue)); + 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, StringComparison.OrdinalIgnoreCase)); - - // If no exists in the Directory.Packages.props file. - if (packageVersionInProps == null) + if (versionOverrideExists != null) { - // Modifying the props file if project is onboarded to CPM. - AddPackageVersionIntoItemGroupCPM(project, libraryDependency); + // Update if VersionOverride instead of Directory.Packages.props file + string packageVersion = libraryDependency.LibraryRange.VersionRange.OriginalString; + UpdateVersionOverride(project, packageReferenceInProps, packageVersion); } else { - // Modify the Directory.Packages.props file with the version that is passed in. - if (!noVersion) + // 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) { - string packageVersion = libraryDependency.LibraryRange.VersionRange.OriginalString; - UpdatePackageVersion(project, packageVersionInProps, packageVersion); + // 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); + } + } } } @@ -422,6 +517,25 @@ private void UpdatePackageReferenceItems(IEnumerable packageReferen } } + /// + /// Updates VersionOverride from element if version is passed in as a CLI argument + /// + /// + /// + /// + internal void UpdateVersionOverride(Project project, ProjectItem packageReference, string versionCLIArgument) + { + // Determine where the item is decalred + ProjectItemElement packageReferenceItemElement = project.GetItemProvenance(packageReference).LastOrDefault()?.ItemElement; + + // Get the Version attribute on the packageVersionItemElement. + ProjectMetadataElement versionOverrideAttribute = packageReferenceItemElement.Metadata.FirstOrDefault(i => i.Name.Equals("VersionOverride")); + + // Update the version + versionOverrideAttribute.Value = versionCLIArgument; + packageReferenceItemElement.ContainingProject.Save(); + } + /// /// Update the element if a version is passed in as a CLI argument. /// diff --git a/src/NuGet.Core/NuGet.LibraryModel/LibraryDependency.cs b/src/NuGet.Core/NuGet.LibraryModel/LibraryDependency.cs index 3a1684e76a8..84a1848c8be 100644 --- a/src/NuGet.Core/NuGet.LibraryModel/LibraryDependency.cs +++ b/src/NuGet.Core/NuGet.LibraryModel/LibraryDependency.cs @@ -124,7 +124,7 @@ public bool Equals(LibraryDependency other) GeneratePathProperty == other.GeneratePathProperty && VersionCentrallyManaged == other.VersionCentrallyManaged && Aliases == other.Aliases && - VersionOverride == other.VersionOverride && + EqualityUtility.EqualsWithNullCheck(VersionOverride, other.VersionOverride) && ReferenceType == other.ReferenceType; } diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs index a271d566614..db724dac282 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetAddPackageTests.cs @@ -685,6 +685,7 @@ private void CopyResourceToDirectory(string resourceName, DirectoryInfo director } } + [Fact] public async Task AddPkg_WithCPM_WhenPackageVersionDoesNotExistAndVersionCLIArgNotPassed_Success() { using var pathContext = new SimpleTestPathContext(); @@ -941,5 +942,769 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( ", propsFileFromDisk); } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_NoVersionOverride_NoPackageVersion_NoVersionCLI_Errors() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} ", ignoreExitCode: true); + + // Assert + Assert.False(result.Success); + Assert.Contains("error: Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items: X", result.Output); + Assert.DoesNotContain(@$" + + ", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_NoVersionOverride_NoPackageVersion_WithVersionCLI_Errors() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version2}", ignoreExitCode: true); + + // Assert + Assert.False(result.Success); + Assert.Contains("error: Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items: X", result.Output); + Assert.DoesNotContain(@$" + + ", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_NoVersionOverride_WithPackageVersion_NoVersionCLI_NoOps() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} ", ignoreExitCode: true); + + // Assert + Assert.False(result.Success); + Assert.DoesNotContain("error: Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items: X", result.Output); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_NoVersionOverride_WithPackageVersion_WithVersionCLI_Success() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version2}", ignoreExitCode: true); + + // Assert + Assert.True(result.Success, result.Output); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_WithVersionOverride_NoPackageVersion_NoVersionCLI_NoOps() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX}", ignoreExitCode: true); + + // Assert + Assert.False(result.Success); + Assert.DoesNotContain("error: Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items: X", result.Output); + Assert.DoesNotContain(@$" + + ", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_WithVersionOverride_NoPackageVersion_WithVersionCLI_Success() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version2}", ignoreExitCode: true); + + // Assert + Assert.True(result.Success, result.Output); + Assert.DoesNotContain(@$" + + ", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_WithVersionOverride_WithPackageVersion_NoVersionCLI_NoOps() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX}", ignoreExitCode: true); + + // Assert + Assert.False(result.Success); + Assert.DoesNotContain("error: Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items: X", result.Output); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_WithVersionOverride_WithPackageVersion_WithVersionCLI_Success() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version2}", ignoreExitCode: true); + + // Assert + Assert.True(result.Success, result.Output); + Assert.DoesNotContain(@$" + + ", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_EmptyVersionOverride_WithPackageVersion_WithVersionCLI_IgnoreEmptyVersionOverride_Success() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = +@$" + + net6.0 + + + + +"; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version2}", ignoreExitCode: true); + + // Assert + Assert.True(result.Success, result.Output); + // Assert + Assert.True(result.Success, result.Output); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.DoesNotContain(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WrongPackageReference_WithVersionOverride_WithPackageVersion_WithVersionCLI_WrongConfiguration_Error() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = + @$" + + net6.0 + + "; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version2}", ignoreExitCode: true); + + // Assert + Assert.False(result.Success); + Assert.Contains("error: Package reference for package 'X' defined in incorrect location, PackageReference should be defined in project file.", result.Output); + Assert.Contains(@$"", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.DoesNotContain(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_WithVersionOverride_WrongPackageVersion_WithVersionCLI_WrongConfiguration_Error() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = + @$" + + net6.0 + + + + + + "; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version2}", ignoreExitCode: true); + + // Assert + Assert.False(result.Success); + Assert.Contains(" PackageVersion for package 'X' defined in incorrect location, PackageVersion should be defined in Directory.Package.props.", result.Output); + Assert.DoesNotContain(@$"", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.Contains(@$" + + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } + + [Fact] + public async Task AddPkg_WithCPM_WithPackageReference_DisabledVersionOverride_WrongPackageVersion_WithVersionCLI_WrongConfiguration_Error() + { + using var pathContext = new SimpleTestPathContext(); + + // Set up solution and project + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + var projectA = XPlatTestUtils.CreateProject("projectA", pathContext, "net6.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); + + // Arrange project file + string projectContent = + @$" + + net6.0 + false + + + + + "; + File.WriteAllText(Path.Combine(pathContext.SolutionRoot, "projectA", "projectA.csproj"), projectContent); + + //Act + var result = _fixture.RunDotnet(projectADirectory, $"add {projectA.ProjectPath} package {packageX} -v {version2}", ignoreExitCode: true); + + // Assert + Assert.False(result.Success); + Assert.Contains("The package reference X specifies a VersionOverride but the ability to override a centrally defined version is currently disabled.", result.Output); + Assert.DoesNotContain(@$"", File.ReadAllText(Path.Combine(pathContext.SolutionRoot, "Directory.Packages.props"))); + Assert.Contains(@$" + + ", File.ReadAllText(Path.Combine(projectADirectory, "projectA.csproj"))); + } } } diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs index eed31acbc85..c145e5416f9 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/MSBuildAPIUtilityTests.cs @@ -21,7 +21,6 @@ static MSBuildAPIUtilityTests() { MSBuildLocator.RegisterDefaults(); } - [PlatformFact(Platform.Windows)] public void GetDirectoryBuildPropsRootElementWhenItExists_Success() { @@ -384,5 +383,74 @@ public void UpdatePackageVersionInPropsFileWhenItExists_Success() Assert.Contains(@$"", updatedPropsFile); Assert.DoesNotContain(@$"", updatedPropsFile); } + + [PlatformFact(Platform.Windows)] + public void UpdateVersionOverrideInPropsFileWhenItExists_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 == "PackageReference" && i.EvaluatedInclude.Equals("X")); + + var libraryDependency = new LibraryDependency + { + LibraryRange = new LibraryRange( + name: "X", + versionRange: VersionRange.Parse("3.0.0"), + typeConstraint: LibraryDependencyTarget.Package) + }; + + // Act + msObject.UpdateVersionOverride(project, packageVersionInProps, "3.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); + } } }