diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/IRestoreResult.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/IRestoreResult.cs new file mode 100644 index 000000000..f9e0adafa --- /dev/null +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/IRestoreResult.cs @@ -0,0 +1,34 @@ +// 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 NuGet.ProjectModel; + +namespace NuGet.Commands +{ + public interface IRestoreResult + { + bool Success { get; } + + /// + /// Gets the path that the lock file will be written to. + /// + string LockFilePath { get; } + + /// + /// Gets a boolean indicating if the lock file will be re-written on + /// because the file needs to be re-locked. + /// + bool RelockFile { get; } + + /// + /// Gets the lock file that was generated during the restore or, in the case of a locked lock file, + /// was used to determine the packages to install during the restore. + /// + LockFile LockFile { get; } + + /// + /// The existing lock file. This is null if no lock file was provided on the . + /// + LockFile PreviousLockFile { get; } + } +} \ No newline at end of file diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/ProjectRestoreCommand.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/ProjectRestoreCommand.cs index 11e5a49f6..f9ee569c3 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/ProjectRestoreCommand.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/ProjectRestoreCommand.cs @@ -316,10 +316,14 @@ namespace NuGet.Commands foreach (var dependency in dependencies) { // Ignore floating or version-less (project) dependencies - if (dependency.LibraryRange.VersionRange != null && !dependency.LibraryRange.VersionRange.IsFloating) + // Avoid warnings for non-packages + if (dependency.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package) + && dependency.LibraryRange.VersionRange != null && !dependency.LibraryRange.VersionRange.IsFloating) { var match = result.Flattened.FirstOrDefault(g => g.Key.Name.Equals(dependency.LibraryRange.Name)); - if (match != null && match.Key.Version > dependency.LibraryRange.VersionRange.MinVersion) + if (match != null + && LibraryTypes.Package == match.Key.Type + && match.Key.Version > dependency.LibraryRange.VersionRange.MinVersion) { _logger.LogWarning(string.Format(CultureInfo.CurrentCulture, Strings.Log_DependencyBumpedUp, dependency.LibraryRange.Name, diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs index a25626b03..ac8aa8860 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs @@ -8,7 +8,6 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -72,16 +71,7 @@ namespace NuGet.Commands Path.Combine(_request.Project.BaseDirectory, LockFileFormat.LockFileName) : _request.LockFilePath; - bool relockFile = false; - if (_request.ExistingLockFile != null - && _request.ExistingLockFile.IsLocked - && !_request.ExistingLockFile.IsValidForPackageSpec(_request.Project, _request.LockFileVersion)) - { - // The lock file was locked, but the project.json is out of date - relockFile = true; - _request.ExistingLockFile.IsLocked = false; - _logger.LogMinimal(Strings.Log_LockFileOutOfDate); - } + var relockFile = ShouldRelockFile(_request.ExistingLockFile, _request.Project); var contextForProject = CreateRemoteWalkContext(_request); @@ -95,27 +85,14 @@ namespace NuGet.Commands toolRestoreResults = await ExecuteToolRestoresAsync(localRepository, token); } - // Build the lock file - LockFile lockFile; - if (_request.ExistingLockFile != null && _request.ExistingLockFile.IsLocked) - { - // No lock file to write! - lockFile = _request.ExistingLockFile; - } - else - { - lockFile = new LockFileBuilder(_request.LockFileVersion, _logger, _includeFlagGraphs) - .CreateLockFile( - _request.ExistingLockFile, - _request.Project, - graphs, - localRepository, - contextForProject, - toolRestoreResults); - - // If the lock file was locked originally but we are re-locking it, well... re-lock it :) - lockFile.IsLocked = relockFile; - } + var lockFile = BuildLockFile( + _request.ExistingLockFile, + _request.Project, + graphs, + localRepository, + contextForProject, + toolRestoreResults, + relockFile); if(!ValidateRestoreGraphs(graphs, _logger)) { @@ -167,6 +144,55 @@ namespace NuGet.Commands toolRestoreResults); } + private LockFile BuildLockFile( + LockFile existingLockFile, + PackageSpec project, + IEnumerable graphs, + NuGetv3LocalRepository localRepository, + RemoteWalkContext contextForProject, + IEnumerable toolRestoreResults, + bool relockFile) + { + // Build the lock file + LockFile lockFile; + if (existingLockFile != null && existingLockFile.IsLocked) + { + // No lock file to write! + lockFile = existingLockFile; + } + else + { + lockFile = new LockFileBuilder(_request.LockFileVersion, _logger, _includeFlagGraphs) + .CreateLockFile( + existingLockFile, + project, + graphs, + localRepository, + contextForProject, + toolRestoreResults); + + // If the lock file was locked originally but we are re-locking it, well... re-lock it :) + lockFile.IsLocked = relockFile; + } + + return lockFile; + } + + private bool ShouldRelockFile(LockFile existingLockFile, PackageSpec project) + { + bool relockFile = false; + if (existingLockFile != null + && existingLockFile.IsLocked + && !existingLockFile.IsValidForPackageSpec(project, _request.LockFileVersion)) + { + // The lock file was locked, but the project.json is out of date + relockFile = true; + existingLockFile.IsLocked = false; + _logger.LogMinimal(Strings.Log_LockFileOutOfDate); + } + return relockFile; + } + private static bool ValidateRestoreGraphs(IEnumerable graphs, ILogger logger) { foreach (var g in graphs) @@ -216,7 +242,6 @@ namespace NuGet.Commands CancellationToken token) { var toolPathResolver = new ToolPathResolver(_request.PackagesDirectory); - var lockFileBuilder = new LockFileBuilder(_request.LockFileVersion, _logger, _includeFlagGraphs); var results = new List(); foreach (var tool in _request.Project.Tools) @@ -251,6 +276,39 @@ namespace NuGet.Commands } }; + // Try to find the existing lock file. Since the existing lock file is pathed under + // a folder that includes the resolved tool's version, this is a bit of a chicken + // and egg problem. That is, we need to run the restore operation in order to resolve + // a tool version, but we need the tool version to find the existing project.lock.json + // file which is required before executing the restore! Fortunately, this is solved by + // looking at the tool's consuming project's lock file to see if the tool has been + // restored before. + LockFile existingToolLockFile = null; + if (_request.ExistingLockFile != null) + { + var existingTarget = _request + .ExistingLockFile + .Tools + .Where(t => t.RuntimeIdentifier == null) + .Where(t => t.TargetFramework.Equals(LockFile.ToolFramework)) + .FirstOrDefault(); + + var existingLibrary = existingTarget?.Libraries + .Where(l => StringComparer.OrdinalIgnoreCase.Equals(l.Name, tool.LibraryRange.Name)) + .Where(l => tool.LibraryRange.VersionRange.Satisfies(l.Version)) + .FirstOrDefault(); + + if (existingLibrary != null) + { + var existingLockFilePath = toolPathResolver.GetLockFilePath( + existingLibrary.Name, + existingLibrary.Version, + existingTarget.TargetFramework); + + existingToolLockFile = LockFileUtilities.GetLockFile(existingLockFilePath, _logger); + } + } + // Execute the restore. var toolSuccess = true; // success for this individual tool restore var runtimeIds = new HashSet(); @@ -261,7 +319,7 @@ namespace NuGet.Commands var projectRestoreRequest = new ProjectRestoreRequest( _request, toolPackageSpec, - null, + existingToolLockFile, new Dictionary(), _runtimeGraphCacheByPackage); var projectRestoreCommand = new ProjectRestoreCommand(_logger, projectRestoreRequest); @@ -283,24 +341,25 @@ namespace NuGet.Commands } // Create the lock file (in memory). - var lockFile = lockFileBuilder.CreateLockFile( - previousLockFile: null, - project: toolPackageSpec, - targetGraphs: graphs, - repository: localRepository, - context: contextForTool, - toolRestoreResults: Enumerable.Empty()); + var toolLockFile = BuildLockFile( + existingToolLockFile, + toolPackageSpec, + graphs, + localRepository, + contextForTool, + Enumerable.Empty(), + false); // Build the path based off of the resolved tool. For now, we assume there is only // ever one target. - var target = lockFile.Targets.Single(); + var target = toolLockFile.Targets.Single(); var fileTargetLibrary = target .Libraries .FirstOrDefault(l => StringComparer.OrdinalIgnoreCase.Equals(tool.LibraryRange.Name, l.Name)); - string lockFilePath = null; + string toolLockFilePath = null; if (fileTargetLibrary != null) { - lockFilePath = toolPathResolver.GetLockFilePath( + toolLockFilePath = toolPathResolver.GetLockFilePath( fileTargetLibrary.Name, fileTargetLibrary.Version, target.TargetFramework); @@ -318,8 +377,9 @@ namespace NuGet.Commands toolSuccess, target, fileTargetLibrary, - lockFilePath, - lockFile)); + toolLockFilePath, + toolLockFile, + existingToolLockFile)); } return results; @@ -651,6 +711,10 @@ namespace NuGet.Commands // Null out all target types, these did not exist in v1 library.Type = null; } + + // Remove tools + lockFile.Tools = null; + lockFile.ProjectFileToolGroups = null; } } } diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreResult.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreResult.cs index 396a8795f..d74d28326 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreResult.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreResult.cs @@ -13,7 +13,7 @@ using NuGet.ProjectModel; namespace NuGet.Commands { - public class RestoreResult + public class RestoreResult : IRestoreResult { public bool Success { get; } @@ -111,54 +111,102 @@ namespace NuGet.Commands /// /// If and are identical /// the file will not be written to disk. - /// The logger. /// Write out the lock file even if no changes exist. - /// The cancellation token. public async Task CommitAsync(ILogger log, bool forceWrite, CancellationToken token) { // Write the lock file var lockFileFormat = new LockFileFormat(); - // Don't write the lock file if it is Locked AND we're not re-locking the file - if (!LockFile.IsLocked || RelockFile) - { - // Avoid writing out the lock file if it is the same to avoid triggering an intellisense - // update on a restore with no actual changes. - if (forceWrite - || PreviousLockFile == null - || !PreviousLockFile.Equals(LockFile)) - { - log.LogMinimal($"Writing lock file to disk. Path: {LockFilePath}"); + await CommitAsync( + lockFileFormat, + result: this, + log: log, + forceWrite: forceWrite, + toolCommit: false, + token: token); - lockFileFormat.Write(LockFilePath, LockFile); - } - else - { - log.LogMinimal($"Lock file has not changed. Skipping lock file write. Path: {LockFilePath}"); - } - } - - // Always write the tool lock files foreach (var toolRestoreResult in ToolRestoreResults) { - if (toolRestoreResult.LockFilePath != null) + if (toolRestoreResult.LockFilePath != null && toolRestoreResult.LockFile != null) { - log.LogDebug($"Writing tool lock file to disk. Path: {toolRestoreResult.LockFilePath}"); - - await ConcurrencyUtilities.ExecuteWithFileLockedAsync( - toolRestoreResult.LockFilePath, - lockedToken => - { - var lockFileDirectory = Path.GetDirectoryName(toolRestoreResult.LockFilePath); - Directory.CreateDirectory(lockFileDirectory); - lockFileFormat.Write(toolRestoreResult.LockFilePath, toolRestoreResult.LockFile); - return Task.FromResult((object)null); - }, - token); + await CommitAsync( + lockFileFormat, + result: toolRestoreResult, + log: log, + forceWrite: forceWrite, + toolCommit: true, + token: token); } } MSBuild.Commit(log); } + + private static async Task CommitAsync( + LockFileFormat lockFileFormat, + IRestoreResult result, + ILogger log, + bool forceWrite, + bool toolCommit, + CancellationToken token) + { + // Don't write the lock file if it is Locked AND we're not re-locking the file + if (!result.LockFile.IsLocked || result.RelockFile || toolCommit) + { + // Avoid writing out the lock file if it is the same to avoid triggering an intellisense + // update on a restore with no actual changes. + if (forceWrite + || result.PreviousLockFile == null + || !result.PreviousLockFile.Equals(result.LockFile)) + { + if (toolCommit) + { + log.LogDebug($"Writing tool lock file to disk. Path: {result.LockFilePath}"); + + await ConcurrencyUtilities.ExecuteWithFileLockedAsync( + result.LockFilePath, + lockedToken => + { + var lockFileDirectory = Path.GetDirectoryName(result.LockFilePath); + Directory.CreateDirectory(lockFileDirectory); + + lockFileFormat.Write(result.LockFilePath, result.LockFile); + + return Task.FromResult(0); + }, + token); + } + else + { + log.LogMinimal($"Writing lock file to disk. Path: {result.LockFilePath}"); + + lockFileFormat.Write(result.LockFilePath, result.LockFile); + } + } + else + { + if (toolCommit) + { + log.LogDebug($"Tool lock file has not changed. Skipping lock file write. Path: {result.LockFilePath}"); + } + else + { + log.LogMinimal($"Lock file has not changed. Skipping lock file write. Path: {result.LockFilePath}"); + } + } + } + } + + private static void WriteLockFile( + LockFileFormat lockFileFormat, + IRestoreResult result, + bool createDirectory) + { + if (createDirectory) + { + } + + lockFileFormat.Write(result.LockFilePath, result.LockFile); + } } } diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/ToolRestoreResult.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/ToolRestoreResult.cs index 822d7224f..70edb174e 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/ToolRestoreResult.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/ToolRestoreResult.cs @@ -5,7 +5,7 @@ using NuGet.ProjectModel; namespace NuGet.Commands { - public class ToolRestoreResult + public class ToolRestoreResult : IRestoreResult { public ToolRestoreResult( string toolName, @@ -13,7 +13,8 @@ namespace NuGet.Commands LockFileTarget lockFileTarget, LockFileTargetLibrary fileTargetLibrary, string lockFilePath, - LockFile lockFile) + LockFile lockFile, + LockFile previousLockFile) { ToolName = toolName; Success = success; @@ -21,6 +22,10 @@ namespace NuGet.Commands FileTargetLibrary = fileTargetLibrary; LockFilePath = lockFilePath; LockFile = lockFile; + PreviousLockFile = previousLockFile; + + // "locked" property is not supported on tools + RelockFile = false; } public string ToolName { get; } @@ -29,5 +34,7 @@ namespace NuGet.Commands public LockFileTargetLibrary FileTargetLibrary { get; } public string LockFilePath { get; } public LockFile LockFile { get; } + public LockFile PreviousLockFile { get; } + public bool RelockFile { get; } } } \ No newline at end of file diff --git a/src/NuGet.Core/NuGet.ProjectModel/LockFile.cs b/src/NuGet.Core/NuGet.ProjectModel/LockFile.cs index 69cb335d1..e288ca701 100644 --- a/src/NuGet.Core/NuGet.ProjectModel/LockFile.cs +++ b/src/NuGet.Core/NuGet.ProjectModel/LockFile.cs @@ -72,9 +72,11 @@ namespace NuGet.ProjectModel } else { - var framework = actualTargetFrameworks - .FirstOrDefault(f => - string.Equals(f.FrameworkName.ToString(), @group.FrameworkName, StringComparison.OrdinalIgnoreCase)); + var framework = actualTargetFrameworks.FirstOrDefault(f => string.Equals( + f.FrameworkName.DotNetFrameworkName, + @group.FrameworkName, + StringComparison.OrdinalIgnoreCase)); + if (framework == null) { return false; @@ -166,7 +168,11 @@ namespace NuGet.ProjectModel group => group.FrameworkName, StringComparer.OrdinalIgnoreCase)) && Libraries.OrderBy(library => library.Name, StringComparer.OrdinalIgnoreCase) .SequenceEqual(other.Libraries.OrderBy(library => library.Name, StringComparer.OrdinalIgnoreCase)) - && Targets.OrderBy(target => target.Name).SequenceEqual(other.Targets.OrderBy(target => target.Name)); + && Targets.OrderBy(target => target.Name).SequenceEqual(other.Targets.OrderBy(target => target.Name)) + && ProjectFileToolGroups.OrderBy(group => group.FrameworkName, StringComparer.OrdinalIgnoreCase) + .SequenceEqual(other.ProjectFileToolGroups.OrderBy( + group => group.FrameworkName, StringComparer.OrdinalIgnoreCase)) + && Tools.OrderBy(target => target.Name).SequenceEqual(other.Tools.OrderBy(target => target.Name)); } public override bool Equals(object obj) @@ -181,23 +187,35 @@ namespace NuGet.ProjectModel combiner.AddObject(IsLocked); combiner.AddObject(Version); - foreach (var item in ProjectFileDependencyGroups.OrderBy( - group => group.FrameworkName, StringComparer.OrdinalIgnoreCase)) - { - combiner.AddObject(item); - } + HashProjectFileDependencyGroups(combiner, ProjectFileDependencyGroups); foreach (var item in Libraries.OrderBy(library => library.Name, StringComparer.OrdinalIgnoreCase)) { combiner.AddObject(item); } - foreach (var item in Targets.OrderBy(target => target.Name, StringComparer.OrdinalIgnoreCase)) - { - combiner.AddObject(item); - } + HashLockFileTargets(combiner, Targets); + HashProjectFileDependencyGroups(combiner, ProjectFileToolGroups); + HashLockFileTargets(combiner, Tools); return combiner.CombinedHash; } + + private static void HashLockFileTargets(HashCodeCombiner combiner, IList targets) + { + foreach (var item in targets.OrderBy(target => target.Name, StringComparer.OrdinalIgnoreCase)) + { + combiner.AddObject(item); + } + } + + private static void HashProjectFileDependencyGroups(HashCodeCombiner combiner, IList groups) + { + foreach (var item in groups.OrderBy( + group => @group.FrameworkName, StringComparer.OrdinalIgnoreCase)) + { + combiner.AddObject(item); + } + } } } diff --git a/src/NuGet.Core/NuGet.ProjectModel/LockFileFormat.cs b/src/NuGet.Core/NuGet.ProjectModel/LockFileFormat.cs index f51f5f527..2d312d878 100644 --- a/src/NuGet.Core/NuGet.ProjectModel/LockFileFormat.cs +++ b/src/NuGet.Core/NuGet.ProjectModel/LockFileFormat.cs @@ -183,8 +183,17 @@ namespace NuGet.ProjectModel json[TargetsProperty] = WriteObject(lockFile.Targets, WriteTarget); json[LibrariesProperty] = WriteObject(lockFile.Libraries, WriteLibrary); json[ProjectFileDependencyGroupsProperty] = WriteObject(lockFile.ProjectFileDependencyGroups, WriteProjectFileDependencyGroup); - json[ToolsProperty] = WriteObject(lockFile.Tools, WriteTarget); - json[ProjectFileToolGroupsProperty] = WriteObject(lockFile.ProjectFileToolGroups, WriteProjectFileDependencyGroup); + + if (lockFile.Tools != null) + { + json[ToolsProperty] = WriteObject(lockFile.Tools, WriteTarget); + } + + if (lockFile.ProjectFileToolGroups != null) + { + json[ProjectFileToolGroupsProperty] = WriteObject(lockFile.ProjectFileToolGroups, WriteProjectFileDependencyGroup); + } + return json; } diff --git a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/compiler/resources/uwpBlankAppV2.json b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/compiler/resources/uwpBlankAppV2.json index 573b1dbac..616885ab0 100644 --- a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/compiler/resources/uwpBlankAppV2.json +++ b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/compiler/resources/uwpBlankAppV2.json @@ -15379,5 +15379,7 @@ "Microsoft.NETCore.UniversalWindowsPlatform >= 5.0.0" ], "UAP,Version=v10.0": [] - } + }, + "tools": {}, + "projectFileToolGroups": {} } \ No newline at end of file diff --git a/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreCommandTests.cs b/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreCommandTests.cs index a274ba6f2..4eb854456 100644 --- a/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreCommandTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreCommandTests.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using NuGet.Configuration; using NuGet.Frameworks; +using NuGet.Logging; using NuGet.ProjectModel; using NuGet.Test.Utility; using Xunit; @@ -247,6 +248,124 @@ namespace NuGet.Commands.Test } } + [Fact] + public async Task RestoreCommand_DoesNotRedoRestoreToolsWithValidLockFile() + { + // Arrange + using (var workingDir = TestFileSystemUtility.CreateRandomTestFolder()) + { + var tc = new ToolTestContext(workingDir) + { + ProjectJson = @" + { + ""frameworks"": { + ""net45"": { } + }, + ""tools"": { + ""packageA"": ""*"" + } + }", + }; + + var packageA = new SimpleTestPackageContext("packageA"); + packageA.AddFile("lib/netstandard1.3/a.dll"); + + SimpleTestPackageUtility.CreatePackages(tc.PackageSource.FullName, packageA); + + tc.Initialize(); + + // the first restore + await (await tc.Command.ExecuteAsync()).CommitAsync(tc.Logger, CancellationToken.None); + + // reset + tc.Logger = new TestLogger(); + tc.Request.Log = tc.Logger; + tc.Initialize(); + tc.Request.ExistingLockFile = LockFileUtilities.GetLockFile(tc.Request.LockFilePath, tc.Logger); + + // Act + var result = await tc.Command.ExecuteAsync(); + await result.CommitAsync(tc.Logger, CancellationToken.None); + + // Assert + Assert.True( + result.Success, + "The command did not succeed. Error messages: " + + Environment.NewLine + tc.Logger.ShowErrors()); + Assert.Equal(1, result.ToolRestoreResults.Count()); + + Assert.Contains( + $"Lock file has not changed. Skipping lock file write. Path: {result.LockFilePath}", + tc.Logger.Messages); + + var toolResult = result.ToolRestoreResults.First(); + Assert.Contains( + $"Tool lock file has not changed. Skipping lock file write. Path: {toolResult.LockFilePath}", + tc.Logger.Messages); + } + } + + [Fact] + public async Task RestoreCommand_ObservesLockedFilesOnlyInProject() + { + // Arrange + using (var workingDir = TestFileSystemUtility.CreateRandomTestFolder()) + { + var tc = new ToolTestContext(workingDir) + { + ProjectJson = @" + { + ""frameworks"": { + ""net45"": { } + }, + ""tools"": { + ""packageA"": ""*"" + } + }", + }; + + var packageA = new SimpleTestPackageContext("packageA"); + packageA.AddFile("lib/netstandard1.3/a.dll"); + + SimpleTestPackageUtility.CreatePackages(tc.PackageSource.FullName, packageA); + + tc.Initialize(); + + // the first restore + var initialResult = await tc.Command.ExecuteAsync(); + await initialResult.CommitAsync(tc.Logger, CancellationToken.None); + + tc.SetIsLocked(initialResult.LockFilePath, true, tc.Logger); + tc.SetIsLocked(initialResult.ToolRestoreResults.First().LockFilePath, true, tc.Logger); + + // reset + tc.Logger = new TestLogger(); + tc.Request.Log = tc.Logger; + tc.Initialize(); + tc.Request.ExistingLockFile = LockFileUtilities.GetLockFile(tc.Request.LockFilePath, tc.Logger); + + // Act + var result = await tc.Command.ExecuteAsync(); + await result.CommitAsync(tc.Logger, CancellationToken.None); + + // Assert + Assert.True( + result.Success, + "The command did not succeed. Error messages: " + + Environment.NewLine + tc.Logger.ShowErrors()); + Assert.Equal(1, result.ToolRestoreResults.Count()); + + // Since the files are locked and valid, the commit is completely skipped. + Assert.DoesNotContain(result.LockFilePath, tc.Logger.ShowMessages()); + + // Locking a tool lock file is not supported. + var toolResult = result.ToolRestoreResults.First(); + Assert.Contains( + $"Tool lock file has not changed. Skipping lock file write. Path: {toolResult.LockFilePath}", + tc.Logger.Messages); + } + } + [Fact] public async Task RestoreCommand_FailsCommandWhenToolRestoreFails() { @@ -379,7 +498,7 @@ namespace NuGet.Commands.Test } public DirectoryInfo PackageSource { get; } - public TestLogger Logger { get; } + public TestLogger Logger { get; set; } public string ProjectJson { private get; set; } public RestoreRequest Request { get; set; } public RestoreCommand Command { get; private set; } @@ -399,6 +518,13 @@ namespace NuGet.Commands.Test Request.LockFilePath = Path.Combine(Project.FullName, "project.lock.json"); Command = new RestoreCommand(Request); } + + public void SetIsLocked(string path, bool isLocked, ILogger logger) + { + var lockFile = LockFileUtilities.GetLockFile(path, logger); + lockFile.IsLocked = isLocked; + new LockFileFormat().Write(path, lockFile); + } } } } diff --git a/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreResultTests.cs b/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreResultTests.cs index 3d7fabc4d..9f84b5ec1 100644 --- a/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreResultTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreResultTests.cs @@ -78,13 +78,16 @@ namespace NuGet.Commands.Test // Arrange using (var td = TestFileSystemUtility.CreateRandomTestFolder()) { - var path = Path.Combine(td, "project.lock.json"); + var path = Path.Combine(td, ".tools", "project.lock.json"); var logger = new TestLogger(); var toolResult = new ToolRestoreResult( + toolName: null, success: true, - restoreGraphs: null, + lockFileTarget: null, + fileTargetLibrary: null, lockFilePath: path, - lockFile: new LockFile()); + lockFile: new LockFile(), + previousLockFile: null); var result = new RestoreResult( success: true, restoreGraphs: null, @@ -104,7 +107,44 @@ namespace NuGet.Commands.Test logger.DebugMessages); Assert.True(File.Exists(path), $"The tool lock file should have been written: {path}"); Assert.Equal(1, logger.DebugMessages.Count); - Assert.Equal(1, logger.MinimalMessages.Count); + } + } + + [Fact] + public async Task RestoreResult_WritesToolSkipCommitToDebug() + { + // Arrange + using (var td = TestFileSystemUtility.CreateRandomTestFolder()) + { + var path = Path.Combine(td, "tools", "project.lock.json"); + var logger = new TestLogger(); + var toolResult = new ToolRestoreResult( + toolName: null, + success: true, + lockFileTarget: null, + fileTargetLibrary: null, + lockFilePath: path, + lockFile: new LockFile(), + previousLockFile: new LockFile()); // same lock file + var result = new RestoreResult( + success: true, + restoreGraphs: null, + compatibilityCheckResults: null, + lockFile: new LockFile(), + previousLockFile: new LockFile(), // same lock file + lockFilePath: null, + msbuild: new MSBuildRestoreResult("project", td, true), + toolRestoreResults: new[] { toolResult }); + + // Act + await result.CommitAsync(logger, CancellationToken.None); + + // Assert + Assert.Contains( + $"Tool lock file has not changed. Skipping lock file write. Path: {path}", + logger.DebugMessages); + Assert.False(File.Exists(path), $"The tool lock file should not have been written: {path}"); + Assert.Equal(1, logger.DebugMessages.Count); } } } diff --git a/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileFormatTests.cs b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileFormatTests.cs index 92208ed05..369dfe1c9 100644 --- a/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileFormatTests.cs +++ b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileFormatTests.cs @@ -12,10 +12,9 @@ namespace NuGet.ProjectModel.Test public class LockFileFormatTests { [Fact] - public void ReadBasicLockFile() + public void LockFileFormat_ReadsLockFileWithNoTools() { - const string lockFileContent = @" -{ + string lockFileContent = @"{ ""locked"": true, ""version"": 1, ""targets"": { @@ -90,44 +89,94 @@ namespace NuGet.ProjectModel.Test var netPlatDepGroup = lockFile.ProjectFileDependencyGroups.Last(); Assert.Equal(NuGetFramework.Parse("dotnet").DotNetFrameworkName, netPlatDepGroup.FrameworkName); Assert.Empty(netPlatDepGroup.Dependencies); + + Assert.Equal(0, lockFile.ProjectFileToolGroups.Count); + Assert.Equal(0, lockFile.Tools.Count); } [Fact] - public void WriteBasicLockFile() + public void LockFileFormat_ReadsLockFileWithTools() + { + string lockFileContent = @"{ + ""tools"": { + "".NETStandard,Version=v1.2"": { + ""System.Runtime/4.0.20-beta-22927"": { + ""dependencies"": { + ""Frob"": ""[4.0.20, )"" + }, + ""compile"": { + ""ref/dotnet/System.Runtime.dll"": {} + } + } + } + }, + ""projectFileToolGroups"": { + "".NETFramework,Version=v4.5.1"": [], + "".NETStandard,Version=v1.5"": [ + ""System.Runtime [4.0.10-beta-*, )"" + ] + } +}"; + var lockFileFormat = new LockFileFormat(); + var lockFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + + var tool = lockFile.Tools.Single(); + Assert.Equal(FrameworkConstants.CommonFrameworks.NetStandard12, tool.TargetFramework); + + var runtimeTargetLibrary = tool.Libraries.Single(); + Assert.Equal("System.Runtime", runtimeTargetLibrary.Name); + Assert.Equal(NuGetVersion.Parse("4.0.20-beta-22927"), runtimeTargetLibrary.Version); + Assert.Equal(0, runtimeTargetLibrary.NativeLibraries.Count); + Assert.Equal(0, runtimeTargetLibrary.ResourceAssemblies.Count); + Assert.Equal(0, runtimeTargetLibrary.FrameworkAssemblies.Count); + Assert.Equal(0, runtimeTargetLibrary.RuntimeAssemblies.Count); + Assert.Equal("ref/dotnet/System.Runtime.dll", runtimeTargetLibrary.CompileTimeAssemblies.Single().Path); + + var net451Group = lockFile.ProjectFileToolGroups.First(); + Assert.Equal(FrameworkConstants.CommonFrameworks.Net451, NuGetFramework.Parse(net451Group.FrameworkName)); + + var netStandardGroup = lockFile.ProjectFileToolGroups.Last(); + Assert.Equal(FrameworkConstants.CommonFrameworks.NetStandard15, NuGetFramework.Parse(netStandardGroup.FrameworkName)); + Assert.Equal("System.Runtime [4.0.10-beta-*, )", netStandardGroup.Dependencies.Single()); + } + + [Fact] + public void LockFileFormat_WritesLockFileWithNoTools() { // Arrange - const string lockFileContent = @"{ + string lockFileContent = @"{ ""locked"": true, ""version"": 2, ""targets"": { - "".NETPlatform,Version=v5.0"": { - ""System.Runtime/4.0.20-beta-22927"": { - ""type"": ""package"", - ""dependencies"": { - ""Frob"": ""4.0.20"" - }, + "".NETPlatform,Version=v5.0"": { + ""System.Runtime/4.0.20-beta-22927"": { + ""type"": ""package"", + ""dependencies"": { + ""Frob"": ""4.0.20"" + }, ""compile"": { - ""ref/dotnet/System.Runtime.dll"": { - } - } - } - } - }, + ""ref/dotnet/System.Runtime.dll"": {} + } + } + } + }, ""libraries"": { - ""System.Runtime/4.0.20-beta-22927"": { - ""sha512"": ""sup3rs3cur3"", + ""System.Runtime/4.0.20-beta-22927"": { + ""sha512"": ""sup3rs3cur3"", ""type"": ""package"", ""files"": [ ""System.Runtime.nuspec"" ] } -}, + }, ""projectFileDependencyGroups"": { """": [ ""System.Runtime [4.0.10-beta-*, )"" ], "".NETPlatform,Version=v5.0"": [] - } + }, + ""tools"": {}, + ""projectFileToolGroups"": {} }"; var lockFile = new LockFile(); lockFile.IsLocked = true; @@ -172,5 +221,69 @@ namespace NuGet.ProjectModel.Test // Assert Assert.Equal(expected.ToString(), output.ToString()); } + + [Fact] + public void LockFileFormat_WritesLockFileWithTools() + { + // Arrange + string lockFileContent = @"{ + ""locked"": true, + ""version"": 2, + ""targets"": {}, + ""libraries"": {}, + ""projectFileDependencyGroups"": {}, + ""tools"": { + "".NETPlatform,Version=v5.0"": { + ""System.Runtime/4.0.20-beta-22927"": { + ""type"": ""package"", + ""dependencies"": { + ""Frob"": ""4.0.20"" + }, + ""compile"": { + ""ref/dotnet/System.Runtime.dll"": {} + } + } + } + }, + ""projectFileToolGroups"": { + """": [ + ""System.Runtime [4.0.10-beta-*, )"" + ], + "".NETPlatform,Version=v5.0"": [] + } +}"; + var lockFile = new LockFile(); + lockFile.IsLocked = true; + lockFile.Version = 2; + + var target = new LockFileTarget() + { + TargetFramework = FrameworkConstants.CommonFrameworks.DotNet + }; + var targetLib = new LockFileTargetLibrary() + { + Name = "System.Runtime", + Version = NuGetVersion.Parse("4.0.20-beta-22927"), + Type = LibraryTypes.Package + }; + targetLib.Dependencies.Add(new NuGet.Packaging.Core.PackageDependency("Frob", + new VersionRange(NuGetVersion.Parse("4.0.20")))); + targetLib.CompileTimeAssemblies.Add(new LockFileItem("ref/dotnet/System.Runtime.dll")); + target.Libraries.Add(targetLib); + lockFile.Tools.Add(target); + + lockFile.ProjectFileToolGroups.Add( + new ProjectFileDependencyGroup("", new string[] { "System.Runtime [4.0.10-beta-*, )" })); + lockFile.ProjectFileToolGroups.Add( + new ProjectFileDependencyGroup(FrameworkConstants.CommonFrameworks.DotNet.DotNetFrameworkName, new string[0])); + + // Act + var lockFileFormat = new LockFileFormat(); + var output = JObject.Parse(lockFileFormat.Render(lockFile)); + var expected = JObject.Parse(lockFileContent); + + // Assert + Assert.Equal(expected.ToString(), output.ToString()); + } } } diff --git a/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileTests.cs b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileTests.cs new file mode 100644 index 000000000..bd3ac5b76 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileTests.cs @@ -0,0 +1,348 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using NuGet.Frameworks; +using NuGet.LibraryModel; +using NuGet.Versioning; +using Xunit; + +namespace NuGet.ProjectModel.Test +{ + public class LockFileTests + { + [Fact] + public void LockFile_ComparesEqualTools() + { + // Arrange + var lockFileA = new LockFile + { + Tools = + { + new LockFileTarget + { + TargetFramework = FrameworkConstants.CommonFrameworks.NetStandard12, + Libraries = { new LockFileTargetLibrary { Name = "SomeLibrary" } } + } + } + }; + + // same thing + var lockFileB = new LockFile + { + Tools = + { + new LockFileTarget + { + TargetFramework = FrameworkConstants.CommonFrameworks.NetStandard12, + Libraries = { new LockFileTargetLibrary { Name = "SomeLibrary" } } + } + } + }; + + // Act & Assert + Assert.True(lockFileA.Equals(lockFileB), "The two lock files should be equal."); + } + + [Fact] + public void LockFile_ComparesDifferentTools() + { + // Arrange + var lockFileA = new LockFile + { + Tools = + { + new LockFileTarget + { + TargetFramework = FrameworkConstants.CommonFrameworks.NetStandard12, + Libraries = { new LockFileTargetLibrary { Name = "SomeLibrary" } } + } + } + }; + + // different thing + var lockFileB = new LockFile + { + Tools = + { + new LockFileTarget + { + TargetFramework = FrameworkConstants.CommonFrameworks.NetStandard12, + Libraries = { new LockFileTargetLibrary { Name = "SomeLibrary" } } + }, + new LockFileTarget + { + TargetFramework = FrameworkConstants.CommonFrameworks.NetStandard12, + Libraries = { new LockFileTargetLibrary { Name = "OtherLibrary" } } + } + } + }; + + // Act & Assert + Assert.False(lockFileA.Equals(lockFileB), "The two lock files should not be equal."); + } + + [Fact] + public void LockFile_ComparesEqualProjectFileToolGroups() + { + // Arrange + var lockFileA = new LockFile + { + ProjectFileToolGroups = + { + new ProjectFileDependencyGroup("FrameworkA", new [] { "DependencyA" }) + } + }; + + // same thing + var lockFileB = new LockFile + { + ProjectFileToolGroups = + { + new ProjectFileDependencyGroup("FrameworkA", new [] { "DependencyA" }) + } + }; + + // Act & Assert + Assert.True(lockFileA.Equals(lockFileB), "The two lock files should be equal."); + } + + [Fact] + public void LockFile_ComparesDifferentProjectFileToolGroups() + { + // Arrange + var lockFileA = new LockFile + { + ProjectFileToolGroups = + { + new ProjectFileDependencyGroup("FrameworkA", new [] { "DependencyA" }) + } + }; + + // different thing + var lockFileB = new LockFile + { + ProjectFileToolGroups = + { + new ProjectFileDependencyGroup("FrameworkA", new [] { "DependencyA" }), + new ProjectFileDependencyGroup("FrameworkB", new [] { "DependencyB" }) + } + }; + + // Act & Assert + Assert.False(lockFileA.Equals(lockFileB), "The two lock files should not be equal."); + } + + [Fact] + public void LockFile_IsValidForPackageSpec_DifferentToolVersions() + { + // Arrange + var lockFile = new LockFile + { + ProjectFileDependencyGroups = + { + new ProjectFileDependencyGroup("", new string[0]) + }, + ProjectFileToolGroups = + { + new ProjectFileDependencyGroup(LockFile.ToolFramework.ToString(), new [] { "DependencyA >= 2.0.0" }) + } + }; + + var packageSpec = new PackageSpec(new JObject()) + { + Dependencies = new List(), + Tools = new List + { + new ToolDependency + { + LibraryRange = new LibraryRange + { + Name = "DependencyA", + TypeConstraint = LibraryDependencyTarget.Package, + VersionRange = VersionRange.Parse("1.0.0") + } + } + } + }; + + // Act & Assert + Assert.False(lockFile.IsValidForPackageSpec(packageSpec)); + } + + [Fact] + public void LockFile_IsValidForPackageSpec_MissingTools() + { + // Arrange + var lockFile = new LockFile + { + ProjectFileDependencyGroups = + { + new ProjectFileDependencyGroup("", new string[0]) + }, + ProjectFileToolGroups = + { + new ProjectFileDependencyGroup(LockFile.ToolFramework.ToString(), new [] { "DependencyA >= 2.0.0" }) + } + }; + + var packageSpec = new PackageSpec(new JObject()) + { + Dependencies = new List(), + Tools = new List + { + new ToolDependency + { + LibraryRange = new LibraryRange + { + Name = "DependencyA", + TypeConstraint = LibraryDependencyTarget.Package, + VersionRange = VersionRange.Parse("1.0.0") + } + }, + new ToolDependency + { + LibraryRange = new LibraryRange + { + Name = "DependencyB", + TypeConstraint = LibraryDependencyTarget.Package, + VersionRange = VersionRange.Parse("2.0.0") + } + } + } + }; + + // Act & Assert + Assert.False(lockFile.IsValidForPackageSpec(packageSpec)); + } + + [Fact] + public void LockFile_IsValidForPackageSpec_ExtraTools() + { + // Arrange + var lockFile = new LockFile + { + ProjectFileDependencyGroups = + { + new ProjectFileDependencyGroup("", new string[0]) + }, + ProjectFileToolGroups = + { + new ProjectFileDependencyGroup(LockFile.ToolFramework.ToString(), new [] + { + "DependencyA >= 1.0.0", + "DependencyB >= 2.0.0", + }) + } + }; + + var packageSpec = new PackageSpec(new JObject()) + { + Dependencies = new List(), + Tools = new List + { + new ToolDependency + { + LibraryRange = new LibraryRange + { + Name = "DependencyA", + TypeConstraint = LibraryDependencyTarget.Package, + VersionRange = VersionRange.Parse("1.0.0") + } + } + } + }; + + // Act & Assert + Assert.False(lockFile.IsValidForPackageSpec(packageSpec)); + } + + [Fact] + public void LockFile_IsValidForPackageSpec_InvalidToolFramework() + { + // Arrange + var lockFile = new LockFile + { + ProjectFileDependencyGroups = + { + new ProjectFileDependencyGroup("", new string[0]) + }, + ProjectFileToolGroups = + { + new ProjectFileDependencyGroup("FrameworkA", new [] + { + "DependencyA >= 1.0.0" + }) + } + }; + + var packageSpec = new PackageSpec(new JObject()) + { + Dependencies = new List(), + Tools = new List + { + new ToolDependency + { + LibraryRange = new LibraryRange + { + Name = "DependencyA", + TypeConstraint = LibraryDependencyTarget.Package, + VersionRange = VersionRange.Parse("1.0.0") + } + } + } + }; + + // Act & Assert + Assert.False(lockFile.IsValidForPackageSpec(packageSpec)); + } + + [Fact] + public void LockFile_IsValidForPackageSpec_MatchingTools() + { + // Arrange + var lockFile = new LockFile + { + ProjectFileDependencyGroups = + { + new ProjectFileDependencyGroup("", new string[0]) + }, + ProjectFileToolGroups = + { + new ProjectFileDependencyGroup(LockFile.ToolFramework.ToString(), new [] + { + "DependencyA >= 1.0.0", + "DependencyB >= 2.0.0", + }) + } + }; + + var packageSpec = new PackageSpec(new JObject()) + { + Dependencies = new List(), + Tools = new List + { + new ToolDependency + { + LibraryRange = new LibraryRange + { + Name = "DependencyB", + TypeConstraint = LibraryDependencyTarget.Package, + VersionRange = VersionRange.Parse("2.0.0") + } + }, + new ToolDependency + { + LibraryRange = new LibraryRange + { + Name = "DependencyA", + TypeConstraint = LibraryDependencyTarget.Package, + VersionRange = VersionRange.Parse("1.0.0") + } + } + } + }; + + // Act & Assert + Assert.True(lockFile.IsValidForPackageSpec(packageSpec)); + } + } +}