From 34ea52068a07dd9dd73bac00c098e21c23fb7a66 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 1 Jun 2017 19:48:45 -0700 Subject: [PATCH] Add --msbuildprojectextensionspath option to dotnet-watch In the event someone wants to move the obj/ folder, MSBuild will not be able to locate dotnet-watch's generated targets. dotnet-watch cannot automatically find the obj folder (#244), so this command line switch allows users to point dotnet-watch to the right location. --- .../CommandLineOptions.cs | 14 +++++++-- .../Internal/MsBuildFileSetFactory.cs | 23 +++++++++----- src/Microsoft.DotNet.Watcher.Tools/Program.cs | 30 +++++++++++++++++-- .../DotNetWatcherTests.cs | 6 ++++ .../Scenario/WatchableApp.cs | 7 ++++- .../KitchenSink/KitchenSink.csproj | 10 ++++++- .../MsBuildFileSetFactoryTest.cs | 6 ++-- 7 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs index e6a6b90fba..af158d0e53 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs @@ -12,6 +12,7 @@ namespace Microsoft.DotNet.Watcher internal class CommandLineOptions { public string Project { get; private set; } + public string MSBuildProjectExtensionsPath { get; private set; } public bool IsHelp { get; private set; } public bool IsQuiet { get; private set; } public bool IsVerbose { get; private set; } @@ -61,8 +62,16 @@ Examples: }; app.HelpOption("-?|-h|--help"); - var optProjects = app.Option("-p|--project", "The project to watch", - CommandOptionType.SingleValue); // TODO multiple shouldn't be too hard to support + // TODO multiple shouldn't be too hard to support + var optProjects = app.Option("-p|--project ", "The project to watch", + CommandOptionType.SingleValue); + + var optMSBuildProjectExtensionsPath = app.Option("--msbuildprojectextensionspath ", + "The MSBuild project extensions path. Defaults to \"obj\".", + CommandOptionType.SingleValue); + // Hide from help text because this is an internal option that will hopefully go away when/if #244 is resolved. + optMSBuildProjectExtensionsPath.ShowInHelpText = false; + var optQuiet = app.Option("-q|--quiet", "Suppresses all output except warnings and errors", CommandOptionType.NoValue); var optVerbose = app.VerboseOption(); @@ -92,6 +101,7 @@ Examples: return new CommandLineOptions { Project = optProjects.Value(), + MSBuildProjectExtensionsPath = optMSBuildProjectExtensionsPath.Value(), IsQuiet = optQuiet.HasValue(), IsVerbose = optVerbose.HasValue(), RemainingArguments = app.RemainingArguments, diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs index a9fd44cece..1c778101db 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs @@ -21,19 +21,26 @@ namespace Microsoft.DotNet.Watcher.Internal private const string WatchTargetsFileName = "DotNetWatchCommon.targets"; private readonly IReporter _reporter; private readonly string _projectFile; + private readonly string _projectExtensionsPath; private readonly string _watchTargetsDir; private readonly OutputSink _outputSink; private readonly ProcessRunner _processRunner; private readonly bool _waitOnError; - public MsBuildFileSetFactory(IReporter reporter, string projectFile, bool waitOnError) - : this(reporter, projectFile, new OutputSink()) + public MsBuildFileSetFactory(IReporter reporter, + string projectFile, + string msBuildProjectExtensionsPath, + bool waitOnError) + : this(reporter, projectFile, msBuildProjectExtensionsPath, new OutputSink()) { _waitOnError = waitOnError; } // output sink is for testing - internal MsBuildFileSetFactory(IReporter reporter, string projectFile, OutputSink outputSink) + internal MsBuildFileSetFactory(IReporter reporter, + string projectFile, + string msBuildProjectExtensionsPath, + OutputSink outputSink) { Ensure.NotNull(reporter, nameof(reporter)); Ensure.NotNullOrEmpty(projectFile, nameof(projectFile)); @@ -44,6 +51,11 @@ namespace Microsoft.DotNet.Watcher.Internal _watchTargetsDir = FindWatchTargetsDir(); _outputSink = outputSink; _processRunner = new ProcessRunner(reporter); + + // default value for MSBuildProjectExtensionsPath is $(BaseIntermediateOutputPath), which defaults to 'obj/'. + _projectExtensionsPath = string.IsNullOrEmpty(msBuildProjectExtensionsPath) + ? Path.Combine(Path.GetDirectoryName(_projectFile), "obj") + : msBuildProjectExtensionsPath; } internal List BuildFlags { get; } = new List @@ -153,11 +165,8 @@ namespace Microsoft.DotNet.Watcher.Internal // Ensures file exists in $(MSBuildProjectExtensionsPath)/$(MSBuildProjectFile).dotnetwatch.targets private void EnsureInitialized() { - // default value for MSBuildProjectExtensionsPath. - var projectExtensionsPath = Path.Combine(Path.GetDirectoryName(_projectFile), "obj"); - // see https://github.com/Microsoft/msbuild/blob/bf9b21cc7869b96ea2289ff31f6aaa5e1d525a26/src/XMakeTasks/Microsoft.Common.targets#L127 - var projectExtensionFile = Path.Combine(projectExtensionsPath, + var projectExtensionFile = Path.Combine(_projectExtensionsPath, Path.GetFileName(_projectFile) + ProjectExtensionFileExtension); if (!File.Exists(projectExtensionFile)) diff --git a/src/Microsoft.DotNet.Watcher.Tools/Program.cs b/src/Microsoft.DotNet.Watcher.Tools/Program.cs index 3b287ed30f..1aea715ddf 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Program.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Program.cs @@ -81,12 +81,14 @@ namespace Microsoft.DotNet.Watcher { return await ListFilesAsync(_reporter, options.Project, + options.MSBuildProjectExtensionsPath, _cts.Token); } else { return await MainInternalAsync(_reporter, options.Project, + options.MSBuildProjectExtensionsPath, options.RemainingArguments, _cts.Token); } @@ -121,6 +123,7 @@ namespace Microsoft.DotNet.Watcher private async Task MainInternalAsync( IReporter reporter, string project, + string msbuildProjectExtensionsPath, ICollection args, CancellationToken cancellationToken) { @@ -136,7 +139,10 @@ namespace Microsoft.DotNet.Watcher return 1; } - var fileSetFactory = new MsBuildFileSetFactory(reporter, projectFile, waitOnError: true); + var fileSetFactory = new MsBuildFileSetFactory(reporter, + projectFile, + NormalizePath(msbuildProjectExtensionsPath), + waitOnError: true); var processInfo = new ProcessSpec { Executable = DotNetMuxer.MuxerPathOrDefault(), @@ -157,6 +163,7 @@ namespace Microsoft.DotNet.Watcher private async Task ListFilesAsync( IReporter reporter, string project, + string msbuildProjectExtensionsPath, CancellationToken cancellationToken) { // TODO multiple projects should be easy enough to add here @@ -171,7 +178,10 @@ namespace Microsoft.DotNet.Watcher return 1; } - var fileSetFactory = new MsBuildFileSetFactory(reporter, projectFile, waitOnError: false); + var fileSetFactory = new MsBuildFileSetFactory(reporter, + projectFile, + NormalizePath(msbuildProjectExtensionsPath), + waitOnError: false); var files = await fileSetFactory.CreateAsync(cancellationToken); if (files == null) @@ -190,6 +200,22 @@ namespace Microsoft.DotNet.Watcher private static IReporter CreateReporter(bool verbose, bool quiet, IConsole console) => new PrefixConsoleReporter(console, verbose || CliContext.IsGlobalVerbose(), quiet); + + private string NormalizePath(string path) + { + if (path == null || Path.IsPathRooted(path)) + { + return path; + } + + if (string.IsNullOrWhiteSpace(path)) + { + return _workingDir; + } + + return Path.Combine(_workingDir, path); + } + public void Dispose() { _console.CancelKeyPress -= OnCancelKeyPress; diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/DotNetWatcherTests.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/DotNetWatcherTests.cs index 1a6884b815..ed89240fb3 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/DotNetWatcherTests.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/DotNetWatcherTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -40,6 +41,11 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests : base("KitchenSink", logger) { } + + protected override IEnumerable GetDefaultArgs() + { + return new[] { "--msbuildprojectextensionspath", ".net/obj", "run", "--" }; + } } } } diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/WatchableApp.cs b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/WatchableApp.cs index 580a7209ce..2b60ef9a17 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/WatchableApp.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/Scenario/WatchableApp.cs @@ -96,7 +96,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests await PrepareAsync(); } - var args = new[] { "run", "--" }.Concat(arguments); + var args = GetDefaultArgs().Concat(arguments); Start(args, name); // Make this timeout long because it depends much on the MSBuild compilation speed. @@ -104,6 +104,11 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests await Process.GetOutputLineAsync(StartedMessage).TimeoutAfter(TimeSpan.FromMinutes(2)); } + protected virtual IEnumerable GetDefaultArgs() + { + return new[] { "run", "--" }; + } + public virtual void Dispose() { _logger?.WriteLine("Disposing WatchableApp"); diff --git a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/KitchenSink/KitchenSink.csproj b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/KitchenSink/KitchenSink.csproj index ce1697ae88..64ce763e70 100755 --- a/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/KitchenSink/KitchenSink.csproj +++ b/test/Microsoft.DotNet.Watcher.Tools.FunctionalTests/TestProjects/KitchenSink/KitchenSink.csproj @@ -1,8 +1,16 @@ - + + + + .net/obj + + + Exe netcoreapp2.0 + + diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs index 41602fc0d8..064dc79902 100644 --- a/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/MsBuildFileSetFactoryTest.cs @@ -238,7 +238,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests graph.Find("A").WithProjectReference(graph.Find("W"), watch: false); var output = new OutputSink(); - var filesetFactory = new MsBuildFileSetFactory(_reporter, graph.GetOrCreate("A").Path, output) + var filesetFactory = new MsBuildFileSetFactory(_reporter, graph.GetOrCreate("A").Path, null, output) { // enables capturing markers to know which projects have been visited BuildFlags = { "/p:_DotNetWatchTraceOutput=true" } @@ -280,7 +280,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests } private Task GetFileSet(TemporaryCSharpProject target) - => GetFileSet(new MsBuildFileSetFactory(_reporter, target.Path, waitOnError: false)); + => GetFileSet(new MsBuildFileSetFactory(_reporter, target.Path, null, waitOnError: false)); private async Task GetFileSet(MsBuildFileSetFactory filesetFactory) { @@ -298,4 +298,4 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests _tempDir.Dispose(); } } -} \ No newline at end of file +}