diff --git a/build_projects/dotnet-cli-build/CompileTargets.cs b/build_projects/dotnet-cli-build/CompileTargets.cs index 6ec05002c..1519019ca 100644 --- a/build_projects/dotnet-cli-build/CompileTargets.cs +++ b/build_projects/dotnet-cli-build/CompileTargets.cs @@ -336,16 +336,11 @@ namespace Microsoft.DotNet.Cli.Build dotnet.Publish("--output", Path.Combine(Dirs.Output, "tools"), "--configuration", configuration) .WorkingDirectory(Path.Combine(c.BuildContext.BuildDirectory, "tools", "Archiver")) .Execute() - .EnsureSuccessful(); + .EnsureSuccessful(); - var packagesToArchive = new List { "-a", intermediateArchive }; - var nuGetPackagesArchiveDirectory = new DirectoryInfo(nuGetPackagesArchiveFolder); - foreach (var directory in nuGetPackagesArchiveDirectory.GetDirectories()) - { - packagesToArchive.Add(directory.FullName); - } - - Cmd(archiver, packagesToArchive) + Cmd(archiver, + "-a", intermediateArchive, + nuGetPackagesArchiveFolder) .Execute(); File.Copy(intermediateArchive, finalArchive); diff --git a/src/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs b/src/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs index d55857038..9b7139464 100644 --- a/src/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs +++ b/src/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs @@ -15,6 +15,8 @@ namespace Microsoft.DotNet.Cli.Utils /// public static string VersionFile => Path.GetFullPath(Path.Combine(typeof(DotnetFiles).GetTypeInfo().Assembly.Location, "..", ".version")); + public static string NuGetPackagesArchive => Path.GetFullPath(Path.Combine(typeof(DotnetFiles).GetTypeInfo().Assembly.Location, "..", "nuGetPackagesArchive.lzma")); + /// /// Reads the version file and adds runtime specific information /// diff --git a/src/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs b/src/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs index 41e3cf012..e67b10cd5 100644 --- a/src/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs +++ b/src/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs @@ -22,11 +22,34 @@ namespace Microsoft.DotNet.Configurer { if(ShouldPrimeNugetCache()) { - Reporter.Output.WriteLine("Configuring dotnet CLI for first time use."); + PrintFirstTimeUseNotice(); + _nugetCachePrimer.PrimeCache(); } } + private void PrintFirstTimeUseNotice() + { + Reporter.Output.WriteLine(); + Reporter.Output.WriteLine("Welcome to .NET Core!"); + Reporter.Output.WriteLine("---------------------"); + Reporter.Output.WriteLine("Learn more about .NET Core @ https://aka.ms/dotnet-docs. " + + "Use dotnet --help to see available commands or go to https://aka.ms/dotnet-cli-docs."); + Reporter.Output.WriteLine("Telemetry"); + Reporter.Output.WriteLine("--------------"); + Reporter.Output.WriteLine("The .NET Core tools collect usage data in order to improve your experience. " + + "The data is anonymous and does not include commandline arguments. " + + "The data is collected by Microsoft and shared with the community."); + Reporter.Output.WriteLine(); + Reporter.Output.WriteLine("You can opt out of telemetry by setting a DOTNET_CLI_TELEMETRY_OPTOUT " + + "environment variable to 1 using your favorite shell."); + Reporter.Output.WriteLine("You can read more about .NET Core tools telemetry @ https://aka.ms/dotnet-cli-telemetry."); + Reporter.Output.WriteLine("Configuring..."); + Reporter.Output.WriteLine("-------------------"); + Reporter.Output.WriteLine("A command is running to initially populate your local package cache, to improve restore" + + "speed and enable offline access. This command will take up to a minute to complete and will only happen once."); + } + private bool ShouldPrimeNugetCache() { return !_nugetCacheSentinel.Exists(); diff --git a/src/Microsoft.DotNet.Configurer/INuGetPackagesArchiver.cs b/src/Microsoft.DotNet.Configurer/INuGetPackagesArchiver.cs index dfe784d60..4fc4b5291 100644 --- a/src/Microsoft.DotNet.Configurer/INuGetPackagesArchiver.cs +++ b/src/Microsoft.DotNet.Configurer/INuGetPackagesArchiver.cs @@ -1,10 +1,14 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; + namespace Microsoft.DotNet.Configurer { - public interface INuGetPackagesArchiver + public interface INuGetPackagesArchiver : IDisposable { - string ExtractArchive(); + string NuGetPackagesArchive { get; } + + string ExtractArchive(); } } diff --git a/src/Microsoft.DotNet.Configurer/NuGetCachePrimer.cs b/src/Microsoft.DotNet.Configurer/NuGetCachePrimer.cs index 5d12671f7..12f93b968 100644 --- a/src/Microsoft.DotNet.Configurer/NuGetCachePrimer.cs +++ b/src/Microsoft.DotNet.Configurer/NuGetCachePrimer.cs @@ -13,6 +13,7 @@ namespace Microsoft.DotNet.Configurer private const string NUGET_SOURCE_PARAMETER = "-s"; private readonly ICommandFactory _commandFactory; private readonly IDirectory _directory; + private readonly IFile _file; private readonly INuGetPackagesArchiver _nugetPackagesArchiver; private readonly INuGetCacheSentinel _nuGetCacheSentinel; @@ -20,7 +21,11 @@ namespace Microsoft.DotNet.Configurer ICommandFactory commandFactory, INuGetPackagesArchiver nugetPackagesArchiver, INuGetCacheSentinel nuGetCacheSentinel) - : this(commandFactory, nugetPackagesArchiver, nuGetCacheSentinel, FileSystemWrapper.Default.Directory) + : this(commandFactory, + nugetPackagesArchiver, + nuGetCacheSentinel, + FileSystemWrapper.Default.Directory, + FileSystemWrapper.Default.File) { } @@ -28,21 +33,33 @@ namespace Microsoft.DotNet.Configurer ICommandFactory commandFactory, INuGetPackagesArchiver nugetPackagesArchiver, INuGetCacheSentinel nuGetCacheSentinel, - IDirectory directory) + IDirectory directory, + IFile file) { _commandFactory = commandFactory; _directory = directory; _nugetPackagesArchiver = nugetPackagesArchiver; _nuGetCacheSentinel = nuGetCacheSentinel; + _file = file; } public void PrimeCache() { + if(SkipPrimingTheCache()) + { + return; + } + var pathToPackagesArchive = _nugetPackagesArchiver.ExtractArchive(); PrimeCacheUsingArchive(pathToPackagesArchive); } + private bool SkipPrimingTheCache() + { + return !_file.Exists(_nugetPackagesArchiver.NuGetPackagesArchive); + } + private void PrimeCacheUsingArchive(string pathToPackagesArchive) { using (var temporaryDotnetNewDirectory = _directory.CreateTemporaryDirectory()) diff --git a/src/Microsoft.DotNet.Configurer/NuGetPackagesArchiver.cs b/src/Microsoft.DotNet.Configurer/NuGetPackagesArchiver.cs index 3c171fd15..64bc7554e 100644 --- a/src/Microsoft.DotNet.Configurer/NuGetPackagesArchiver.cs +++ b/src/Microsoft.DotNet.Configurer/NuGetPackagesArchiver.cs @@ -1,20 +1,40 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Archive; +using Microsoft.Extensions.EnvironmentAbstractions; + namespace Microsoft.DotNet.Configurer { public class NuGetPackagesArchiver : INuGetPackagesArchiver { - public string ExtractArchive() - { - // -- ExtractArchive - // find archive - // extract archive to temporary folder - // Path.GetTempPath(); - // Path.GetRandomFileName(); - // Consider putting this inside an abstraction that will delete the folder automatically once it is done. + private ITemporaryDirectory _temporaryDirectory; - return @"C:\Users\licavalc\git\temp\feed"; + public string NuGetPackagesArchive => DotnetFiles.NuGetPackagesArchive; + + public NuGetPackagesArchiver() : this(FileSystemWrapper.Default.Directory) + { } + + internal NuGetPackagesArchiver(IDirectory directory) + { + _temporaryDirectory = directory.CreateTemporaryDirectory(); + } + + public string ExtractArchive() + { + var progress = new ConsoleProgressReport(); + var archive = new IndexedArchive(); + + archive.Extract(NuGetPackagesArchive, _temporaryDirectory.DirectoryPath, progress); + + return _temporaryDirectory.DirectoryPath; + } + + public void Dispose() + { + _temporaryDirectory.Dispose(); + } } -} +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Configurer/project.json b/src/Microsoft.DotNet.Configurer/project.json index 8e7ad4709..cc3e093c3 100644 --- a/src/Microsoft.DotNet.Configurer/project.json +++ b/src/Microsoft.DotNet.Configurer/project.json @@ -13,14 +13,14 @@ }, "Microsoft.DotNet.ProjectModel": { "target": "project" + }, + "Microsoft.DotNet.Archive": { + "target": "project" } }, "frameworks": { - "netcoreapp1.0": { + "netstandard1.6": { "imports": [ - "dnxcore50", - "netstandardapp1.5", - "portable-net45+win8", "portable-net45+wp80+win8+wpa81+dnxcore50" ] } diff --git a/src/dotnet/Program.cs b/src/dotnet/Program.cs index 7ca1b9ce6..54863d2ad 100644 --- a/src/dotnet/Program.cs +++ b/src/dotnet/Program.cs @@ -57,13 +57,7 @@ namespace Microsoft.DotNet.Cli { using (PerfTrace.Current.CaptureTiming()) { - var nugetCacheSentinel = new NuGetCacheSentinel(); - var nugetPackagesArchiver = new NuGetPackagesArchiver(); - var commandFactory = new DotNetCommandFactory(); - var nugetCachePrimer = new NuGetCachePrimer(commandFactory, nugetPackagesArchiver, nugetCacheSentinel); - var dotnetConfigurer = new DotnetFirstTimeUseConfigurer(nugetCachePrimer, nugetCacheSentinel); - - dotnetConfigurer.Configure(); + ConfigureDotNetForFirstTimeUse(); return ProcessArgs(args, new Telemetry()); } @@ -166,6 +160,18 @@ namespace Microsoft.DotNet.Cli } + private static void ConfigureDotNetForFirstTimeUse() + { + using (var nugetPackagesArchiver = new NuGetPackagesArchiver()) + { + var nugetCacheSentinel = new NuGetCacheSentinel(); + var commandFactory = new DotNetCommandFactory(); + var nugetCachePrimer = new NuGetCachePrimer(commandFactory, nugetPackagesArchiver, nugetCacheSentinel); + var dotnetConfigurer = new DotnetFirstTimeUseConfigurer(nugetCachePrimer, nugetCacheSentinel); + + dotnetConfigurer.Configure(); + } + } private static void InitializeProcess() { diff --git a/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCachePrimer.cs b/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCachePrimer.cs index d20b1eaf4..4e0b81477 100644 --- a/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCachePrimer.cs +++ b/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCachePrimer.cs @@ -16,6 +16,7 @@ namespace Microsoft.DotNet.Configurer.UnitTests { public class GivenANuGetCachePrimer { + private const string COMPRESSED_ARCHIVE_PATH = "a path to somewhere"; private const string TEMPORARY_FOLDER_PATH = "some path"; private const string PACKAGES_ARCHIVE_PATH = "some other path"; @@ -32,6 +33,7 @@ namespace Microsoft.DotNet.Configurer.UnitTests { var fileSystemMockBuilder = FileSystemMockBuilder.Create(); fileSystemMockBuilder.TemporaryFolder = TEMPORARY_FOLDER_PATH; + fileSystemMockBuilder.AddFile(COMPRESSED_ARCHIVE_PATH); _fileSystemMock = fileSystemMockBuilder.Build(); _temporaryDirectoryMock = (ITemporaryDirectoryMock)_fileSystemMock.Directory.CreateTemporaryDirectory(); @@ -39,6 +41,7 @@ namespace Microsoft.DotNet.Configurer.UnitTests _nugetPackagesArchiverMock = new Mock(); _nugetPackagesArchiverMock.Setup(n => n.ExtractArchive()).Returns(PACKAGES_ARCHIVE_PATH); + _nugetPackagesArchiverMock.Setup(n => n.NuGetPackagesArchive).Returns(COMPRESSED_ARCHIVE_PATH); _nugetCacheSentinel = new Mock(); @@ -46,7 +49,8 @@ namespace Microsoft.DotNet.Configurer.UnitTests _commandFactoryMock.Object, _nugetPackagesArchiverMock.Object, _nugetCacheSentinel.Object, - _fileSystemMock.Directory); + _fileSystemMock.Directory, + _fileSystemMock.File); nugetCachePrimer.PrimeCache(); } @@ -83,6 +87,34 @@ namespace Microsoft.DotNet.Configurer.UnitTests commandMock.Setup(c => c.CaptureStdErr()).Returns(commandMock.Object); } + [Fact] + public void It_does_not_prime_the_NuGet_cache_if_the_archive_is_not_found_so_that_we_do_not_need_to_generate_the_archive_for_stage1() + { + var fileSystemMockBuilder = FileSystemMockBuilder.Create(); + var fileSystemMock = fileSystemMockBuilder.Build(); + + var commandFactoryMock = SetupCommandFactoryMock(); + + var nugetPackagesArchiverMock = new Mock(); + nugetPackagesArchiverMock.Setup(n => n.NuGetPackagesArchive).Returns(COMPRESSED_ARCHIVE_PATH); + + var nugetCachePrimer = new NuGetCachePrimer( + commandFactoryMock.Object, + nugetPackagesArchiverMock.Object, + _nugetCacheSentinel.Object, + fileSystemMock.Directory, + fileSystemMock.File); + + nugetCachePrimer.PrimeCache(); + + nugetPackagesArchiverMock.Verify(n => n.ExtractArchive(), Times.Never); + commandFactoryMock.Verify(c => c.Create( + It.IsAny(), + It.IsAny>(), + null, + Constants.DefaultConfiguration), Times.Never); + } + [Fact] public void It_disposes_the_temporary_directory_created_for_the_temporary_project_used_to_prime_the_cache() { @@ -135,7 +167,8 @@ namespace Microsoft.DotNet.Configurer.UnitTests commandFactoryMock.Object, _nugetPackagesArchiverMock.Object, _nugetCacheSentinel.Object, - _fileSystemMock.Directory); + _fileSystemMock.Directory, + _fileSystemMock.File); nugetCachePrimer.PrimeCache(); @@ -188,7 +221,8 @@ namespace Microsoft.DotNet.Configurer.UnitTests _commandFactoryMock.Object, _nugetPackagesArchiverMock.Object, nugetCacheSentinel.Object, - _fileSystemMock.Directory); + _fileSystemMock.Directory, + _fileSystemMock.File); nugetCachePrimer.PrimeCache(); diff --git a/test/dotnet-publish.Tests/PublishTests.cs b/test/dotnet-publish.Tests/PublishTests.cs index f23bdc6b6..924bad75b 100644 --- a/test/dotnet-publish.Tests/PublishTests.cs +++ b/test/dotnet-publish.Tests/PublishTests.cs @@ -142,15 +142,17 @@ namespace Microsoft.DotNet.Tools.Publish.Tests [Fact] public void CrossPublishingSucceedsAndHasExpectedArtifacts() - { - var testNugetCache = "packages_cross_publish_test"; + { TestInstance instance = TestAssetsManager.CreateTestInstance(Path.Combine("PortableTests")); - + var testProject = Path.Combine(instance.TestRoot, "StandaloneApp", "project.json"); + var workingDirectory = Path.GetDirectoryName(testProject); + var testNugetCache = Path.Combine(workingDirectory, "packages_cross_publish_test"); var restoreCommand = new RestoreCommand(); - restoreCommand.WorkingDirectory = Path.GetDirectoryName(testProject); + restoreCommand.WorkingDirectory = workingDirectory; + restoreCommand.Environment["NUGET_PACKAGES"] = testNugetCache; restoreCommand.Execute().Should().Pass(); diff --git a/test/dotnet.Tests/GivenThatTheUserIsRunningDoNetForTheFirstTime.cs b/test/dotnet.Tests/GivenThatTheUserIsRunningDoNetForTheFirstTime.cs new file mode 100644 index 000000000..0a0487837 --- /dev/null +++ b/test/dotnet.Tests/GivenThatTheUserIsRunningDoNetForTheFirstTime.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved.  +// Licensed under the MIT license. See LICENSE file in the project root for full license information.  + +using System; +using System.IO; +using System.Collections.Generic; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.TestFramework; +using Microsoft.DotNet.Tools.Test.Utilities; +using Xunit; +using FluentAssertions; + +namespace Microsoft.DotNet.Tests +{ + public class GivenThatTheUserIsRunningDotNetForTheFirstTime : TestBase + { + private static CommandResult _firstDotnetUseCommandResult; + private static DirectoryInfo _nugetCacheFolder; + + static GivenThatTheUserIsRunningDotNetForTheFirstTime() + { + var testDirectory = TestAssetsManager.CreateTestDirectory("Dotnet_first_time_experience_tests"); + var testNugetCache = Path.Combine(testDirectory.Path, "nuget_cache"); + + var command = new DotnetCommand() + .WithWorkingDirectory(testDirectory.Path); + command.Environment["NUGET_PACKAGES"] = testNugetCache; + + _firstDotnetUseCommandResult = command.ExecuteWithCapturedOutput("new"); + + _nugetCacheFolder = new DirectoryInfo(testNugetCache); + } + + [Fact] + public void Using_dotnet_for_the_first_time_succeeds() + { + _firstDotnetUseCommandResult.Should().Pass(); + } + + [Fact] + public void It_shows_the_appropriate_message_to_the_user() + { + const string firstTimeUseWelcomeMessage = @"Welcome to .NET Core! +--------------------- +Learn more about .NET Core @ https://aka.ms/dotnet-docs. Use dotnet --help to see available commands or go to https://aka.ms/dotnet-cli-docs. +Telemetry +-------------- +The .NET Core tools collect usage data in order to improve your experience. The data is anonymous and does not include commandline arguments. The data is collected by Microsoft and shared with the community. +You can opt out of telemetry by setting a DOTNET_CLI_TELEMETRY_OPTOUT environment variable to 1 using your favorite shell. +You can read more about .NET Core tools telemetry @ https://aka.ms/dotnet-cli-telemetry. +Configuring... +------------------- +A command is running to initially populate your local package cache, to improve restorespeed and enable offline access. This command will take up to a minute to complete and will only happen once."; + + _firstDotnetUseCommandResult.StdOut.Should().StartWith(firstTimeUseWelcomeMessage); + } + + [Fact] + public void It_restores_the_nuget_packages_to_the_nuget_cache_folder() + { + _nugetCacheFolder.Should().HaveFile($"{GetDotnetVersion()}.dotnetSentinel"); + } + + [Fact] + public void It_creates_a_sentinel_file_under_the_nuget_cache_folder() + { + _nugetCacheFolder.Should().HaveDirectory("Microsoft.NETCore.App"); + } + + private string GetDotnetVersion() + { + return new DotnetCommand().ExecuteWithCapturedOutput("--version").StdOut + .TrimEnd(Environment.NewLine.ToCharArray()); + } + } +} \ No newline at end of file