diff --git a/src/Tasks.UnitTests/OutputPathTests.cs b/src/Tasks.UnitTests/OutputPathTests.cs
new file mode 100644
index 00000000000..f2bc410bbbd
--- /dev/null
+++ b/src/Tasks.UnitTests/OutputPathTests.cs
@@ -0,0 +1,186 @@
+// Copyright (c) Microsoft. 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 Microsoft.Build.Evaluation;
+using Microsoft.Build.Shared;
+using Microsoft.Build.UnitTests;
+
+using Shouldly;
+
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Build.Tasks.UnitTests
+{
+ public sealed class OutputPathTests : IDisposable
+ {
+ private readonly ITestOutputHelper _output;
+ private readonly string _projectRelativePath = Path.Combine("src", "test", "test.csproj");
+
+ public OutputPathTests(ITestOutputHelper output)
+ {
+ _output = output;
+ ObjectModelHelpers.DeleteTempProjectDirectory();
+ }
+
+ public void Dispose()
+ {
+ ObjectModelHelpers.DeleteTempProjectDirectory();
+ }
+
+ ///
+ /// Test when both BaseOutputPath and OutputPath are not specified.
+ ///
+ [Fact]
+ public void BothBaseOutputPathAndOutputPathWereNotSpecified()
+ {
+ // Arrange
+ var baseOutputPath = "bin";
+
+ var projectFilePath = ObjectModelHelpers.CreateFileInTempProjectDirectory(_projectRelativePath,
+$@"
+
+
+
+
+ AnyCPU
+ Debug
+
+
+
+
+
+");
+
+ // Act
+ Project project = ObjectModelHelpers.LoadProjectFileInTempProjectDirectory(projectFilePath, touchProject: false);
+
+ project.Build(new MockLogger(_output)).ShouldBeFalse();
+
+ // Assert
+ project.GetPropertyValue("BaseOutputPath").ShouldBe(baseOutputPath.WithTrailingSlash());
+ project.GetPropertyValue("BaseOutputPathWasSpecified").ShouldBe(string.Empty);
+ project.GetPropertyValue("_OutputPathWasMissing").ShouldBe("true");
+ }
+
+ ///
+ /// Test when BaseOutputPath is specified without the OutputPath.
+ ///
+ [Fact]
+ public void BaseOutputPathWasSpecifiedAndIsOverridable()
+ {
+ // Arrange
+ var baseOutputPath = Path.Combine("build", "bin");
+
+ var projectFilePath = ObjectModelHelpers.CreateFileInTempProjectDirectory(_projectRelativePath,
+$@"
+
+
+
+
+ AnyCPU
+ Debug
+ {baseOutputPath}
+
+
+
+
+
+");
+
+ // Act
+ Project project = ObjectModelHelpers.LoadProjectFileInTempProjectDirectory(projectFilePath, touchProject: false);
+
+ project.Build(new MockLogger(_output)).ShouldBeTrue();
+
+ // Assert
+ project.GetPropertyValue("BaseOutputPath").ShouldBe(baseOutputPath.WithTrailingSlash());
+ project.GetPropertyValue("BaseOutputPathWasSpecified").ShouldBe("true");
+ project.GetPropertyValue("_OutputPathWasMissing").ShouldBe("true");
+ }
+
+ ///
+ /// Test when both BaseOutputPath and OutputPath are specified.
+ ///
+ [Fact]
+ public void BothBaseOutputPathAndOutputPathWereSpecified()
+ {
+ // Arrange
+ var baseOutputPath = Path.Combine("build", "bin");
+ var outputPath = Path.Combine("bin", "Debug");
+ var outputPathAlt = Path.Combine("bin", "Release");
+
+ var projectFilePath = ObjectModelHelpers.CreateFileInTempProjectDirectory(_projectRelativePath,
+$@"
+
+
+
+
+ AnyCPU
+ Debug
+
+
+
+ {baseOutputPath}
+ {outputPath}
+ {outputPathAlt}
+
+
+
+
+
+");
+
+ // Act
+ Project project = ObjectModelHelpers.LoadProjectFileInTempProjectDirectory(projectFilePath, touchProject: false);
+
+ project.Build(new MockLogger(_output)).ShouldBeTrue();
+
+ // Assert
+ project.GetPropertyValue("BaseOutputPath").ShouldBe(baseOutputPath.WithTrailingSlash());
+ project.GetPropertyValue("OutputPath").ShouldBe(outputPath.WithTrailingSlash());
+ project.GetPropertyValue("BaseOutputPathWasSpecified").ShouldBe("true");
+ project.GetPropertyValue("_OutputPathWasMissing").ShouldBe(string.Empty);
+ }
+
+ ///
+ /// Test for [MSBuild]::NormalizePath and [MSBuild]::NormalizeDirectory returning current directory instead of current Project directory.
+ ///
+ [ConditionalFact(typeof(NativeMethodsShared), nameof(NativeMethodsShared.IsWindows), Skip = "Skipping this test for now until we have a consensus about this issue.")]
+ public void MSBuildNormalizePathShouldReturnProjectDirectory()
+ {
+ // Arrange
+ var configuration = "Debug";
+ var baseOutputPath = "bin";
+
+ var projectFilePath = ObjectModelHelpers.CreateFileInTempProjectDirectory(_projectRelativePath,
+$@"
+
+
+
+
+ $([MSBuild]::NormalizeDirectory('{baseOutputPath}', '{configuration}'))
+
+
+
+
+
+");
+
+ // Act
+ Project project = ObjectModelHelpers.LoadProjectFileInTempProjectDirectory(projectFilePath, touchProject: false);
+
+ project.Build(new MockLogger(_output)).ShouldBeTrue();
+
+ // Assert
+ project.GetPropertyValue("Configuration").ShouldBe(configuration);
+ project.GetPropertyValue("BaseOutputPath").ShouldBe(baseOutputPath.WithTrailingSlash());
+
+ var expectedOutputPath = FileUtilities.CombinePaths(project.DirectoryPath, baseOutputPath, configuration).WithTrailingSlash();
+ project.GetPropertyValue("OutputPath").ShouldBe(expectedOutputPath);
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.Common.CrossTargeting.targets b/src/Tasks/Microsoft.Common.CrossTargeting.targets
index 51ef08021b0..c7023d3cca6 100644
--- a/src/Tasks/Microsoft.Common.CrossTargeting.targets
+++ b/src/Tasks/Microsoft.Common.CrossTargeting.targets
@@ -201,7 +201,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
<_DirectoryBuildTargetsFile Condition="'$(_DirectoryBuildTargetsFile)' == ''">Directory.Build.targets
<_DirectoryBuildTargetsBasePath Condition="'$(_DirectoryBuildTargetsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildTargetsFile)'))
- $([System.IO.Path]::Combine('$(_DirectoryBuildTargetsBasePath)', '$(_DirectoryBuildTargetsFile)'))
+ $([MSBuild]::NormalizePath('$(_DirectoryBuildTargetsBasePath)', '$(_DirectoryBuildTargetsFile)'))
diff --git a/src/Tasks/Microsoft.Common.CurrentVersion.targets b/src/Tasks/Microsoft.Common.CurrentVersion.targets
index a006a2f3083..c7c94603ad1 100644
--- a/src/Tasks/Microsoft.Common.CurrentVersion.targets
+++ b/src/Tasks/Microsoft.Common.CurrentVersion.targets
@@ -68,6 +68,8 @@ Copyright (C) Microsoft Corporation. All rights reserved.
+
+ true
true
@@ -109,8 +111,16 @@ Copyright (C) Microsoft Corporation. All rights reserved.
OutDir can be used to gather multiple project outputs in one location. In addition,
OutDir is included in AssemblySearchPaths used for resolving references.
+ BaseOutputPath:
+ This is the top level folder where all configuration specific output folders will be created.
+ Default value is bin\
+
OutputPath:
- This property is usually specified in the project file and is used to initialize OutDir.
+ This is the full Output Path, and is derived from BaseOutputPath, if none specified
+ (eg. bin\Debug). If this property is overridden, then setting BaseOutputPath has no effect.
+
+ For Legacy projects using only Common targets, this property is usually specified in the project file
+ and is used to initialize OutDir. Some SDKs including the .NET SDK derive this automatically.
OutDir and OutputPath are distinguished for legacy reasons, and OutDir should be used if at all possible.
BaseIntermediateOutputPath:
@@ -119,26 +129,42 @@ Copyright (C) Microsoft Corporation. All rights reserved.
IntermediateOutputPath:
This is the full intermediate Output Path, and is derived from BaseIntermediateOutputPath, if none specified
- (eg. obj\debug). If this property is overridden, then setting BaseIntermediateOutputPath has no effect.
+ (eg. obj\Debug). If this property is overridden, then setting BaseIntermediateOutputPath has no effect.
+
+ Ensure any and all path property has a trailing slash, so it can be concatenated.
-->
- true
-
- $(OutputPath)\
- $(MSBuildProjectName)
-
- bin\Debug\
- <_OriginalConfiguration>$(Configuration)
+
<_OriginalPlatform>$(Platform)
- Debug
- $(Configuration)
- AnyCPU
+ <_OriginalConfiguration>$(Configuration)
+
+ <_OutputPathWasMissing Condition="'$(_OriginalPlatform)' != '' and '$(_OriginalConfiguration)' != '' and '$(OutputPath)' == ''">true
+
+ true
+
+
+
+ AnyCPU
+ $(Platform)
+ Debug
+ $(Configuration)
+
+ $([MSBuild]::EnsureTrailingSlash($([MSBuild]::ValueOrDefault('$(BaseOutputPath)', 'bin'))))
+ $([System.IO.Path]::Combine('$(BaseOutputPath)', '$(Configuration)'))
+ $([System.IO.Path]::Combine('$(BaseOutputPath)', '$(PlatformName)', '$(Configuration)'))
+ $([MSBuild]::EnsureTrailingSlash('$(OutputPath)'))
+
+ $([MSBuild]::EnsureTrailingSlash($([MSBuild]::ValueOrDefault('$(BaseIntermediateOutputPath)', 'obj'))))
+ $([System.IO.Path]::Combine('$(BaseIntermediateOutputPath)', '$(Configuration)'))
+ $([System.IO.Path]::Combine('$(BaseIntermediateOutputPath)', '$(PlatformName)', '$(Configuration)'))
+ $([MSBuild]::EnsureTrailingSlash('$(IntermediateOutputPath)'))
+
+
+
+
$(TargetType)
library
exe
@@ -162,12 +188,20 @@ Copyright (C) Microsoft Corporation. All rights reserved.
false
-
+
+
+ When 'OutputPath' is missing or empty (along with non-existent 'BaseOutputPath') at this point means,
+ we're in legacy mode and we shall assume the current Configuration/Platform combination as invalid.
+ Whether this is considered an error or a warning depends on the value of $(SkipInvalidConfigurations).
+ -->
<_InvalidConfigurationError Condition=" '$(SkipInvalidConfigurations)' != 'true' ">true
<_InvalidConfigurationWarning Condition=" '$(SkipInvalidConfigurations)' == 'true' ">true
@@ -176,7 +210,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
IDE Macros available from both integrated builds and from command line builds.
The following properties are 'macros' that are available via IDE for
pre and post build steps.
- -->
+ -->
.exe
.exe
@@ -191,16 +225,16 @@ Copyright (C) Microsoft Corporation. All rights reserved.
true
- $(OutputPath)
- $(OutDir)\
+ $([MSBuild]::EnsureTrailingSlash($([MSBuild]::ValueOrDefault('$(OutDir)', '$(OutputPath)'))))
$(MSBuildProjectName)
- $(OutDir)$(ProjectName)\
+ $([MSBuild]::EnsureTrailingSlash('$(OutDir)$(ProjectName)'))
+ $(MSBuildProjectName)
$(RootNamespace)
$(AssemblyName)
@@ -279,26 +313,23 @@ Copyright (C) Microsoft Corporation. All rights reserved.
-
+
- $([MSBuild]::Escape($([System.IO.Path]::GetFullPath(`$([System.IO.Path]::Combine(`$(MSBuildProjectDirectory)`, `$(OutDir)`))`))))
+ $([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(OutDir)'))
-
+
$(TargetDir)$(TargetFileName)
$([MSBuild]::NormalizePath($(TargetDir), 'ref', $(TargetFileName)))
-
- $(MSBuildProjectDirectory)\
+
+ $([MSBuild]::EnsureTrailingSlash($(MSBuildProjectDirectory)))
-
+
$(ProjectDir)$(ProjectFileName)
-
-
- $(Platform)
@@ -336,7 +367,6 @@ Copyright (C) Microsoft Corporation. All rights reserved.
false
- $(BaseIntermediateOutputPath)\
$(MSBuildProjectFile).FileListAbsolute.txt
false
@@ -349,12 +379,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
false
-
- $(BaseIntermediateOutputPath)$(Configuration)\
- $(BaseIntermediateOutputPath)$(PlatformName)\$(Configuration)\
-
- $(IntermediateOutputPath)\
<_GenerateBindingRedirectsIntermediateAppConfig>$(IntermediateOutputPath)$(TargetFileName).config
@@ -377,12 +402,12 @@ Copyright (C) Microsoft Corporation. All rights reserved.
$(IntermediateOutputPath)$(TargetName).pdb
- <_WinMDDebugSymbolsOutputPath>$([System.IO.Path]::Combine('$(OutDir)', $([System.IO.Path]::GetFileName('$(WinMDExpOutputPdb)'))))
+ <_WinMDDebugSymbolsOutputPath>$(OutDir)$([System.IO.Path]::GetFileName('$(WinMDExpOutputPdb)'))
$(IntermediateOutputPath)$(TargetName).xml
- <_WinMDDocFileOutputPath>$([System.IO.Path]::Combine('$(OutDir)', $([System.IO.Path]::GetFileName('$(WinMDOutputDocumentationFile)'))))
+ <_WinMDDocFileOutputPath>$(OutDir)$([System.IO.Path]::GetFileName('$(WinMDOutputDocumentationFile)'))
@@ -454,8 +479,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
- $(PublishDir)\
- $(OutputPath)app.publish\
+ $([MSBuild]::EnsureTrailingSlash($([MSBuild]::ValueOrDefault('$(PublishDir)', '$(OutputPath)app.publish'))))
-
+
+
+
-
+
@(_TargetFrameworkInfo->'%(TargetPlatformMonikers)')
true
-
+
false
true
@@ -3363,7 +3389,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
true
- $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))
+ $(IntermediateOutputPath)$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)
@@ -4003,7 +4029,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
Name="_DeploymentGenerateLauncher"
Condition="'$(GenerateClickOnceManifests)'=='true' and '$(_DeploymentLauncherBased)' == 'true'">
-
@@ -4568,7 +4594,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
-
+
diff --git a/src/Tasks/Microsoft.Common.props b/src/Tasks/Microsoft.Common.props
index 89e402b0582..942daa68814 100644
--- a/src/Tasks/Microsoft.Common.props
+++ b/src/Tasks/Microsoft.Common.props
@@ -26,7 +26,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
<_DirectoryBuildPropsFile Condition="'$(_DirectoryBuildPropsFile)' == ''">Directory.Build.props
<_DirectoryBuildPropsBasePath Condition="'$(_DirectoryBuildPropsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildPropsFile)'))
- $([System.IO.Path]::Combine('$(_DirectoryBuildPropsBasePath)', '$(_DirectoryBuildPropsFile)'))
+ $([MSBuild]::NormalizePath('$(_DirectoryBuildPropsBasePath)', '$(_DirectoryBuildPropsFile)'))
@@ -43,18 +43,16 @@ Copyright (C) Microsoft Corporation. All rights reserved.
The declaration of $(BaseIntermediateOutputPath) had to be moved up from Microsoft.Common.CurrentVersion.targets
in order for the $(MSBuildProjectExtensionsPath) to use it as a default.
-->
- obj\
- $(BaseIntermediateOutputPath)\
+ $([MSBuild]::EnsureTrailingSlash($([MSBuild]::ValueOrDefault('$(BaseIntermediateOutputPath)', 'obj'))))
<_InitialBaseIntermediateOutputPath>$(BaseIntermediateOutputPath)
- $(BaseIntermediateOutputPath)
+ $([MSBuild]::EnsureTrailingSlash($([MSBuild]::ValueOrDefault('$(MSBuildProjectExtensionsPath)', '$(BaseIntermediateOutputPath)'))))
- $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)'))
- $(MSBuildProjectExtensionsPath)\
+ $([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)'))
true
<_InitialMSBuildProjectExtensionsPath Condition=" '$(ImportProjectExtensionProps)' == 'true' ">$(MSBuildProjectExtensionsPath)
diff --git a/src/Tasks/Microsoft.Common.targets b/src/Tasks/Microsoft.Common.targets
index d88e7eb9221..b3e9be1fa09 100644
--- a/src/Tasks/Microsoft.Common.targets
+++ b/src/Tasks/Microsoft.Common.targets
@@ -137,7 +137,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
<_DirectoryBuildTargetsFile Condition="'$(_DirectoryBuildTargetsFile)' == ''">Directory.Build.targets
<_DirectoryBuildTargetsBasePath Condition="'$(_DirectoryBuildTargetsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildTargetsFile)'))
- $([System.IO.Path]::Combine('$(_DirectoryBuildTargetsBasePath)', '$(_DirectoryBuildTargetsFile)'))
+ $([MSBuild]::NormalizePath('$(_DirectoryBuildTargetsBasePath)', '$(_DirectoryBuildTargetsFile)'))