|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Build.Collections;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Shared;
using Xunit;
using Xunit.NetCore.Extensions;
#nullable disable
namespace Microsoft.Build.UnitTests.Evaluation
{
/// <summary>
/// Unit tests for Importing from $(MSBuildExtensionsPath*)
/// </summary>
public class ImportFromMSBuildExtensionsPathTests : IDisposable
{
private string toolsVersionToUse = null;
public ImportFromMSBuildExtensionsPathTests()
{
using var collection = new ProjectCollection();
toolsVersionToUse = collection.DefaultToolsVersion;
}
public void Dispose()
{
ToolsetConfigurationReaderTestHelper.CleanUp();
}
[Fact]
public void ImportFromExtensionsPathFound()
{
CreateAndBuildProjectForImportFromExtensionsPath("MSBuildExtensionsPath", (p, l) => Assert.True(p.Build()));
}
[Fact]
public void ImportFromExtensionsPathNotFound()
{
string extnDir1 = null;
string mainProjectPath = null;
try
{
extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"), GetExtensionTargetsFileContent1());
mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", GetMainTargetFileContent());
using var projColln = GetProjectCollection();
projColln.ResetToolsetsForTests(WriteConfigFileAndGetReader("MSBuildExtensionsPath", extnDir1, Path.Combine("tmp", "nonexistent")));
var logger = new MockLogger();
projColln.RegisterLogger(logger);
Assert.Throws<InvalidProjectFileException>(() => projColln.LoadProject(mainProjectPath));
logger.AssertLogContains("MSB4226");
}
finally
{
if (mainProjectPath != null)
{
FileUtilities.DeleteNoThrow(mainProjectPath);
}
if (extnDir1 != null)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir1, recursive: true);
}
}
}
[Fact]
public void ConditionalImportFromExtensionsPathNotFound()
{
string extnTargetsFileContentWithCondition = @"
<Project>
<PropertyGroup>
<PropertyFromExtn1>FooBar</PropertyFromExtn1>
</PropertyGroup>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
<Import Project='$(MSBuildExtensionsPath)\bar\extn2.proj' Condition=""Exists('$(MSBuildExtensionsPath)\bar\extn2.proj')""/>
</Project>
";
string extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"), extnTargetsFileContentWithCondition);
string mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", GetMainTargetFileContent());
CreateAndBuildProjectForImportFromExtensionsPath(mainProjectPath, "MSBuildExtensionsPath", new string[] { extnDir1, Path.Combine("tmp", "nonexistent") },
null,
(p, l) =>
{
Assert.True(p.Build());
l.AssertLogContains("Running FromExtn");
l.AssertLogContains("PropertyFromExtn1: FooBar");
});
}
[Fact]
public void ImportFromExtensionsPathCircularImportError()
{
string extnTargetsFileContent1 = @"
<Project>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
<Import Project='$(MSBuildExtensionsPath)\foo\extn2.proj' />
</Project>
";
string extnTargetsFileContent2 = @"
<Project>
<Target Name='FromExtn2'>
<Message Text='Running FromExtn'/>
</Target>
<Import Project='{0}'/>
</Project>
";
string mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", GetMainTargetFileContent());
string extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"), extnTargetsFileContent1);
string extnDir2 = GetNewExtensionsPathAndCreateFile("extensions2", Path.Combine("foo", "extn2.proj"),
String.Format(extnTargetsFileContent2, mainProjectPath));
CreateAndBuildProjectForImportFromExtensionsPath(mainProjectPath, "MSBuildExtensionsPath",
new string[] { extnDir2, Path.Combine("tmp", "nonexistent"), extnDir1 },
null,
(p, l) => l.AssertLogContains("MSB4210"));
}
[Fact]
public void ExtensionPathFallbackIsCaseInsensitive()
{
string mainTargetsFileContent = @"
<Project>
<Target Name='Main'>
<Message Text='Running Main'/>
</Target>
<Import Project='$(msbuildExtensionsPath)\foo\extn.proj'/>
</Project>";
string extnTargetsFileContent = @"
<Project>
<Target Name='FromExtn'>
<Message Text='Running {0}'/>
</Target>
</Project>
";
string extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"), extnTargetsFileContent);
string mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", mainTargetsFileContent);
CreateAndBuildProjectForImportFromExtensionsPath(mainProjectPath, "MSBuildExtensionsPath",
new[] { extnDir1 },
null,
(project, logger) =>
{
Console.WriteLine(logger.FullLog);
Console.WriteLine("checking FromExtn");
Assert.True(project.Build("FromExtn"));
Console.WriteLine("checking logcontains");
logger.AssertLogDoesntContain("MSB4057"); // Should not contain TargetDoesNotExist
});
}
[Fact]
public void ImportFromExtensionsPathWithWildCard()
{
string mainTargetsFileContent = @"
<Project>
<Target Name='Main'>
<Message Text='Running Main'/>
</Target>
<Import Project='$(MSBuildExtensionsPath)\foo\*.proj'/>
</Project>";
string extnTargetsFileContent = @"
<Project>
<Target Name='{0}'>
<Message Text='Running {0}'/>
</Target>
</Project>
";
// Importing a wildcard will union all matching results from all fallback locations.
string extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"),
string.Format(extnTargetsFileContent, "FromExtn1"));
string extnDir2 = GetNewExtensionsPathAndCreateFile("extensions2", Path.Combine("foo", "extn.proj"),
string.Format(extnTargetsFileContent, "FromExtn2"));
string mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", mainTargetsFileContent);
CreateAndBuildProjectForImportFromExtensionsPath(mainProjectPath, "MSBuildExtensionsPath",
new[] { extnDir1, Path.Combine("tmp", "nonexistent"), extnDir2 },
null,
(project, logger) =>
{
Console.WriteLine(logger.FullLog);
Console.WriteLine("checking FromExtn1");
Assert.True(project.Build("FromExtn1"));
Console.WriteLine("checking FromExtn2");
Assert.True(project.Build("FromExtn2"));
Console.WriteLine("checking logcontains");
logger.AssertLogDoesntContain("MSB4057"); // Should not contain TargetDoesNotExist
});
}
[Fact]
public void ImportFromExtensionsPathWithWildCardAndSelfImport()
{
string mainTargetsFileContent = @"
<Project>
<Target Name='Main'>
<Message Text='Running Main'/>
</Target>
<Import Project='$(MSBuildExtensionsPath)\circularwildcardtest\*.proj'/>
</Project>";
string extnTargetsFileContent = @"
<Project>
<Target Name='{0}'>
<Message Text='Running {0}'/>
</Target>
</Project>";
string extnTargetsFileContent2 = @"
<Project>
<Import Project='$(MSBuildExtensionsPath)\circularwildcardtest\*.proj'/>
<Target Name='{0}'>
<Message Text='Running {0}'/>
</Target>
</Project>";
// Importing a wildcard will union all matching results from all fallback locations.
string extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("circularwildcardtest", "extn.proj"),
string.Format(extnTargetsFileContent, "FromExtn1"));
string extnDir2 = GetNewExtensionsPathAndCreateFile("extensions2", Path.Combine("circularwildcardtest", "extn.proj"),
string.Format(extnTargetsFileContent, "FromExtn2"));
string extnDir3 = GetNewExtensionsPathAndCreateFile("extensions3", Path.Combine("circularwildcardtest", "extn3.proj"),
string.Format(extnTargetsFileContent2, "FromExtn3"));
// Main project path is under "circularwildcardtest"
// Note: This project will try to be imported again and cause a warning (MSB4210). This test should ensure that the
// code does not stop looking in the fallback locations when this happens (extn3.proj should still be imported).
string mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory(Path.Combine("extensions2", "circularwildcardtest", "main.proj"), mainTargetsFileContent);
CreateAndBuildProjectForImportFromExtensionsPath(mainProjectPath, "MSBuildExtensionsPath",
new[] { extnDir1, extnDir2, extnDir3 },
null,
(project, logger) =>
{
Console.WriteLine(logger.FullLog);
Assert.True(project.Build("FromExtn1"));
Assert.True(project.Build("FromExtn2"));
Assert.True(project.Build("FromExtn3"));
logger.AssertLogContains("MSB4210");
});
}
[Fact]
public void ImportFromExtensionsPathWithWildCardNothingFound()
{
string extnTargetsFileContent = @"
<Project>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
<Import Project='$(MSBuildExtensionsPath)\non-existent\*.proj'/>
</Project>
";
string extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"), extnTargetsFileContent);
string mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", GetMainTargetFileContent());
CreateAndBuildProjectForImportFromExtensionsPath(mainProjectPath, "MSBuildExtensionsPath", new string[] { Path.Combine("tmp", "nonexistent"), extnDir1 },
null, (p, l) => Assert.True(p.Build()));
}
[Fact]
public void ImportFromExtensionsPathInvalidFile()
{
string extnTargetsFileContent = @"<Project>";
string extnDir1 = null;
string mainProjectPath = null;
try
{
extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"), extnTargetsFileContent);
mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", GetMainTargetFileContent());
using var projColln = GetProjectCollection();
projColln.ResetToolsetsForTests(WriteConfigFileAndGetReader("MSBuildExtensionsPath", extnDir1,
Path.Combine("tmp", "nonexistent")));
var logger = new MockLogger();
projColln.RegisterLogger(logger);
Assert.Throws<InvalidProjectFileException>(() => projColln.LoadProject(mainProjectPath));
logger.AssertLogContains("MSB4024");
}
finally
{
if (mainProjectPath != null)
{
FileUtilities.DeleteNoThrow(mainProjectPath);
}
if (extnDir1 != null)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir1, recursive: true);
}
}
}
[Fact]
public void ImportFromExtensionsPathSearchOrder()
{
string extnTargetsFileContent1 = @"
<Project>
<PropertyGroup>
<PropertyFromExtn1>FromFirstFile</PropertyFromExtn1>
</PropertyGroup>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
</Project>
";
string extnTargetsFileContent2 = @"
<Project>
<PropertyGroup>
<PropertyFromExtn1>FromSecondFile</PropertyFromExtn1>
</PropertyGroup>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
</Project>
";
// File with the same name available in two different extension paths, but the one from the first
// directory in MSBuildExtensionsPath environment variable should get loaded
string extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"), extnTargetsFileContent1);
string extnDir2 = GetNewExtensionsPathAndCreateFile("extensions2", Path.Combine("foo", "extn.proj"), extnTargetsFileContent2);
string mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", GetMainTargetFileContent());
CreateAndBuildProjectForImportFromExtensionsPath(mainProjectPath, "MSBuildExtensionsPath", new string[] { extnDir2, Path.Combine("tmp", "nonexistent"), extnDir1 },
null,
(p, l) =>
{
Assert.True(p.Build());
l.AssertLogContains("Running FromExtn");
l.AssertLogContains("PropertyFromExtn1: FromSecondFile");
});
}
[Fact]
public void ImportFromExtensionsPathSearchOrder2()
{
string extnTargetsFileContent1 = @"
<Project>
<PropertyGroup>
<PropertyFromExtn1>FromFirstFile</PropertyFromExtn1>
</PropertyGroup>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
</Project>
";
string extnTargetsFileContent2 = @"
<Project>
<PropertyGroup>
<PropertyFromExtn1>FromSecondFile</PropertyFromExtn1>
</PropertyGroup>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
</Project>
";
// File with the same name available in two different extension paths, but the one from the first
// directory in MSBuildExtensionsPath environment variable should get loaded
string extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"), extnTargetsFileContent1);
string extnDir2 = GetNewExtensionsPathAndCreateFile("extensions2", Path.Combine("foo", "extn.proj"), extnTargetsFileContent2);
string mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", GetMainTargetFileContent());
// MSBuildExtensionsPath* property value has highest priority for the lookups
try
{
using var projColln = GetProjectCollection();
projColln.ResetToolsetsForTests(WriteConfigFileAndGetReader("MSBuildExtensionsPath", Path.Combine("tmp", "non-existent"), extnDir1));
var logger = new MockLogger();
projColln.RegisterLogger(logger);
var project = projColln.LoadProject(mainProjectPath);
project.SetProperty("MSBuildExtensionsPath", extnDir2);
project.ReevaluateIfNecessary();
Assert.True(project.Build());
logger.AssertLogContains("Running FromExtn");
logger.AssertLogContains("PropertyFromExtn1: FromSecondFile");
}
finally
{
if (mainProjectPath != null)
{
FileUtilities.DeleteNoThrow(mainProjectPath);
}
if (extnDir1 != null)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir1, recursive: true);
}
if (extnDir2 != null)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir2, recursive: true);
}
}
}
[Fact]
public void ImportOrderFromExtensionsPath32()
{
CreateAndBuildProjectForImportFromExtensionsPath("MSBuildExtensionsPath32", (p, l) => Assert.True(p.Build()));
}
[Fact]
public void ImportOrderFromExtensionsPath64()
{
CreateAndBuildProjectForImportFromExtensionsPath("MSBuildExtensionsPath64", (p, l) => Assert.True(p.Build()));
}
// Use MSBuildExtensionsPath, MSBuildExtensionsPath32 and MSBuildExtensionsPath64 in the build
[Fact]
public void ImportFromExtensionsPathAnd32And64()
{
string extnTargetsFileContentTemplate = @"
<Project>
<Target Name='FromExtn{0}' DependsOnTargets='{1}'>
<Message Text='Running FromExtn{0}'/>
</Target>
{2}
</Project>
";
var configFileContents = @"
<configuration>
<configSections>
<section name=""msbuildToolsets"" type=""Microsoft.Build.Evaluation.ToolsetConfigurationSection, Microsoft.Build"" />
</configSections>
<msbuildToolsets default=""" + toolsVersionToUse + @""">
<toolset toolsVersion=""" + toolsVersionToUse + @""">
<property name=""MSBuildToolsPath"" value="".""/>
<property name=""MSBuildBinPath"" value=""" + /*v4Folder*/"." + @"""/>
<projectImportSearchPaths>
<searchPaths os=""" + NativeMethodsShared.GetOSNameForExtensionsPath() + @""">
<property name=""MSBuildExtensionsPath"" value=""{0}"" />
<property name=""MSBuildExtensionsPath32"" value=""{1}"" />
<property name=""MSBuildExtensionsPath64"" value=""{2}"" />
</searchPaths>
</projectImportSearchPaths>
</toolset>
</msbuildToolsets>
</configuration>";
string extnDir1 = null, extnDir2 = null, extnDir3 = null;
string mainProjectPath = null;
try
{
extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"),
String.Format(extnTargetsFileContentTemplate, String.Empty, "FromExtn2", "<Import Project='$(MSBuildExtensionsPath32)\\bar\\extn2.proj' />"));
extnDir2 = GetNewExtensionsPathAndCreateFile("extensions2", Path.Combine("bar", "extn2.proj"),
String.Format(extnTargetsFileContentTemplate, 2, "FromExtn3", "<Import Project='$(MSBuildExtensionsPath64)\\xyz\\extn3.proj' />"));
extnDir3 = GetNewExtensionsPathAndCreateFile("extensions3", Path.Combine("xyz", "extn3.proj"),
String.Format(extnTargetsFileContentTemplate, 3, String.Empty, String.Empty));
mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", GetMainTargetFileContent());
ToolsetConfigurationReaderTestHelper.WriteConfigFile(String.Format(configFileContents, extnDir1, extnDir2, extnDir3));
var reader = GetStandardConfigurationReader();
using var projColln = GetProjectCollection();
projColln.ResetToolsetsForTests(reader);
var logger = new MockLogger();
projColln.RegisterLogger(logger);
var project = projColln.LoadProject(mainProjectPath);
Assert.True(project.Build("Main"));
logger.AssertLogContains("Running FromExtn3");
logger.AssertLogContains("Running FromExtn2");
logger.AssertLogContains("Running FromExtn");
}
finally
{
if (mainProjectPath != null)
{
FileUtilities.DeleteNoThrow(mainProjectPath);
}
if (extnDir1 != null)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir1, recursive: true);
}
if (extnDir2 != null)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir2, recursive: true);
}
if (extnDir3 != null)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir3, recursive: true);
}
}
}
// Fall-back path that has a property in it: $(FallbackExpandDir1)
[Fact]
public void ExpandExtensionsPathFallback()
{
string extnTargetsFileContentTemplate = @"
<Project>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
<Import Project='$(MSBuildExtensionsPath)\\foo\\extn.proj' Condition=""Exists('$(MSBuildExtensionsPath)\foo\extn.proj')"" />
</Project>";
var configFileContents = @"
<configuration>
<configSections>
<section name=""msbuildToolsets"" type=""Microsoft.Build.Evaluation.ToolsetConfigurationSection, Microsoft.Build"" />
</configSections>
<msbuildToolsets default=""" + toolsVersionToUse + @""">
<toolset toolsVersion=""" + toolsVersionToUse + @""">
<property name=""MSBuildToolsPath"" value="".""/>
<property name=""MSBuildBinPath"" value="".""/>
<projectImportSearchPaths>
<searchPaths os=""" + NativeMethodsShared.GetOSNameForExtensionsPath() + @""">
<property name=""MSBuildExtensionsPath"" value=""$(FallbackExpandDir1)"" />
</searchPaths>
</projectImportSearchPaths>
</toolset>
</msbuildToolsets>
</configuration>";
string extnDir1 = null;
string mainProjectPath = null;
try
{
extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"),
extnTargetsFileContentTemplate);
mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj",
GetMainTargetFileContent());
ToolsetConfigurationReaderTestHelper.WriteConfigFile(configFileContents);
var reader = GetStandardConfigurationReader();
using var projectCollection = GetProjectCollection(new Dictionary<string, string> { ["FallbackExpandDir1"] = extnDir1 });
projectCollection.ResetToolsetsForTests(reader);
var logger = new MockLogger();
projectCollection.RegisterLogger(logger);
var project = projectCollection.LoadProject(mainProjectPath);
Assert.True(project.Build("Main"));
logger.AssertLogContains("Running FromExtn");
}
finally
{
FileUtilities.DeleteNoThrow(mainProjectPath);
FileUtilities.DeleteDirectoryNoThrow(extnDir1, true);
}
}
// Fall-back path that has a property in it: $(FallbackExpandDir1)
[Fact]
public void ExpandExtensionsPathFallbackInErrorMessage()
{
string extnTargetsFileContentTemplate = @"
<Project>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
<Import Project='$(MSBuildExtensionsPath)\\foo\\extn2.proj' Condition=""Exists('$(MSBuildExtensionsPath)\foo\extn.proj')"" />
</Project>";
var configFileContents = @"
<configuration>
<configSections>
<section name=""msbuildToolsets"" type=""Microsoft.Build.Evaluation.ToolsetConfigurationSection, Microsoft.Build"" />
</configSections>
<msbuildToolsets default=""" + toolsVersionToUse + @""">
<toolset toolsVersion=""" + toolsVersionToUse + @""">
<property name=""MSBuildToolsPath"" value="".""/>
<property name=""MSBuildBinPath"" value="".""/>
<projectImportSearchPaths>
<searchPaths os=""" + NativeMethodsShared.GetOSNameForExtensionsPath() + @""">
<property name=""MSBuildExtensionsPath"" value=""$(FallbackExpandDir1)"" />
</searchPaths>
</projectImportSearchPaths>
</toolset>
</msbuildToolsets>
</configuration>";
string extnDir1 = null;
string mainProjectPath = null;
try
{
extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"),
extnTargetsFileContentTemplate);
mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj",
GetMainTargetFileContent());
ToolsetConfigurationReaderTestHelper.WriteConfigFile(configFileContents);
var reader = GetStandardConfigurationReader();
using var projectCollection = GetProjectCollection(new Dictionary<string, string> { ["FallbackExpandDir1"] = extnDir1 });
projectCollection.ResetToolsetsForTests(reader);
var logger = new MockLogger();
projectCollection.RegisterLogger(logger);
Assert.Throws<InvalidProjectFileException>(() => projectCollection.LoadProject(mainProjectPath));
// Expanded $(FallbackExpandDir) will appear in quotes in the log
logger.AssertLogContains("\"" + extnDir1 + "\"");
}
finally
{
FileUtilities.DeleteNoThrow(mainProjectPath);
FileUtilities.DeleteDirectoryNoThrow(extnDir1, true);
}
}
// Fall-back search path with custom variable
[Fact]
public void FallbackImportWithIndirectReference()
{
string mainTargetsFileContent = @"
<Project>
<PropertyGroup>
<VSToolsPath>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v99</VSToolsPath>
</PropertyGroup>
<Import Project='$(VSToolsPath)\DNX\Microsoft.DNX.Props' Condition=""Exists('$(VSToolsPath)\DNX\Microsoft.DNX.Props')"" />
<Target Name='Main' DependsOnTargets='FromExtn' />
</Project>";
string extnTargetsFileContentTemplate = @"
<Project>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
</Project>";
var configFileContents = @"
<configuration>
<configSections>
<section name=""msbuildToolsets"" type=""Microsoft.Build.Evaluation.ToolsetConfigurationSection, Microsoft.Build"" />
</configSections>
<msbuildToolsets default=""" + toolsVersionToUse + @""">
<toolset toolsVersion=""" + toolsVersionToUse + @""">
<property name=""MSBuildToolsPath"" value="".""/>
<property name=""MSBuildBinPath"" value="".""/>
<projectImportSearchPaths>
<searchPaths os=""" + NativeMethodsShared.GetOSNameForExtensionsPath() + @""">
<property name=""MSBuildExtensionsPath"" value=""$(FallbackExpandDir1)"" />
<property name=""VSToolsPath"" value=""$(FallbackExpandDir1)\Microsoft\VisualStudio\v99"" />
</searchPaths>
</projectImportSearchPaths>
</toolset>
</msbuildToolsets>
</configuration>";
string extnDir1 = null;
string mainProjectPath = null;
try
{
extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("Microsoft", "VisualStudio", "v99", "DNX", "Microsoft.DNX.Props"),
extnTargetsFileContentTemplate);
mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", mainTargetsFileContent);
ToolsetConfigurationReaderTestHelper.WriteConfigFile(configFileContents);
var reader = GetStandardConfigurationReader();
using var projectCollection = GetProjectCollection(new Dictionary<string, string> { ["FallbackExpandDir1"] = extnDir1 });
projectCollection.ResetToolsetsForTests(reader);
var logger = new MockLogger();
projectCollection.RegisterLogger(logger);
var project = projectCollection.LoadProject(mainProjectPath);
Assert.True(project.Build("Main"));
logger.AssertLogContains("Running FromExtn");
}
finally
{
FileUtilities.DeleteNoThrow(mainProjectPath);
FileUtilities.DeleteDirectoryNoThrow(extnDir1, true);
}
}
// Fall-back search path on a property that is not defined.
[Fact]
public void FallbackImportWithUndefinedProperty()
{
string mainTargetsFileContent = @"
<Project>
<Import Project='$(UndefinedProperty)\file.props' Condition=""Exists('$(UndefinedProperty)\file.props')"" />
<Target Name='Main' DependsOnTargets='FromExtn' />
</Project>";
string extnTargetsFileContentTemplate = @"
<Project>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
</Project>";
var configFileContents = @"
<configuration>
<configSections>
<section name=""msbuildToolsets"" type=""Microsoft.Build.Evaluation.ToolsetConfigurationSection, Microsoft.Build"" />
</configSections>
<msbuildToolsets default=""" + toolsVersionToUse + @""">
<toolset toolsVersion=""" + toolsVersionToUse + @""">
<property name=""MSBuildToolsPath"" value="".""/>
<property name=""MSBuildBinPath"" value="".""/>
<projectImportSearchPaths>
<searchPaths os=""" + NativeMethodsShared.GetOSNameForExtensionsPath() + @""">
<property name=""UndefinedProperty"" value=""$(FallbackExpandDir1)"" />
</searchPaths>
</projectImportSearchPaths>
</toolset>
</msbuildToolsets>
</configuration>";
string extnDir1 = null;
string mainProjectPath = null;
try
{
extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("file.props"),
extnTargetsFileContentTemplate);
mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", mainTargetsFileContent);
ToolsetConfigurationReaderTestHelper.WriteConfigFile(configFileContents);
var reader = GetStandardConfigurationReader();
using var projectCollection = GetProjectCollection(new Dictionary<string, string> { ["FallbackExpandDir1"] = extnDir1 });
projectCollection.ResetToolsetsForTests(reader);
var logger = new MockLogger();
projectCollection.RegisterLogger(logger);
var project = projectCollection.LoadProject(mainProjectPath);
Assert.True(project.Build("Main"));
logger.AssertLogContains("Running FromExtn");
}
finally
{
FileUtilities.DeleteNoThrow(mainProjectPath);
FileUtilities.DeleteDirectoryNoThrow(extnDir1, true);
}
}
[Fact]
public void FallbackImportWithFileNotFoundWhenPropertyNotDefined()
{
// Import something from $(UndefinedProperty)
string mainTargetsFileContent = @"
<Project>
<Import Project='$(UndefinedProperty)\filenotfound.props' />
<Target Name='Main' DependsOnTargets='FromExtn' />
</Project>";
string extnDir1 = null;
string mainProjectPath = null;
try
{
// The path to "extensions1" fallback should exist, but the file doesn't need to
extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("file.props"), string.Empty);
// Implement fallback for UndefinedProperty, but don't define the property.
var configFileContents = @"
<configuration>
<configSections>
<section name=""msbuildToolsets"" type=""Microsoft.Build.Evaluation.ToolsetConfigurationSection, Microsoft.Build, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"" />
</configSections>
<msbuildToolsets default=""" + toolsVersionToUse + @""">
<toolset toolsVersion=""" + toolsVersionToUse + @""">
<property name=""MSBuildToolsPath"" value="".""/>
<property name=""MSBuildBinPath"" value="".""/>
<projectImportSearchPaths>
<searchPaths os=""" + NativeMethodsShared.GetOSNameForExtensionsPath() + @""">
<property name=""UndefinedProperty"" value=""" + extnDir1 + @""" />
</searchPaths>
</projectImportSearchPaths>
</toolset>
</msbuildToolsets>
</configuration>";
mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", mainTargetsFileContent);
ToolsetConfigurationReaderTestHelper.WriteConfigFile(configFileContents);
var reader = GetStandardConfigurationReader();
using var projectCollection = GetProjectCollection(new Dictionary<string, string> { ["FallbackExpandDir1"] = extnDir1 });
projectCollection.ResetToolsetsForTests(reader);
var logger = new MockLogger();
projectCollection.RegisterLogger(logger);
Assert.Throws<InvalidProjectFileException>(() => projectCollection.LoadProject(mainProjectPath));
logger.AssertLogContains(@"MSB4226: The imported project """ + Path.Combine("$(UndefinedProperty)", "filenotfound.props")
+ @""" was not found. Also, tried to find");
}
finally
{
FileUtilities.DeleteNoThrow(mainProjectPath);
FileUtilities.DeleteDirectoryNoThrow(extnDir1, true);
}
}
/// <summary>
/// Fall-back search path on a property that is not valid. https://github.com/dotnet/msbuild/issues/8762
/// </summary>
/// <param name="projectValue">imported project value expression</param>
[Theory]
[InlineData("")]
[InlineData("|")]
public void FallbackImportWithInvalidProjectValue(string projectValue)
{
string mainTargetsFileContent = $"""
<Project>
<PropertyGroup>
<VSToolsPath>{projectValue}</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)"/>
</Project>
""";
using TestEnvironment testEnvironment = TestEnvironment.Create();
string mainProjectPath = testEnvironment.CreateTestProjectWithFiles("main.proj", mainTargetsFileContent).ProjectFile;
using var projectCollection = GetProjectCollection();
projectCollection.ResetToolsetsForTests(WriteConfigFileAndGetReader("VSToolsPath", "temp"));
var logger = new MockLogger();
projectCollection.RegisterLogger(logger);
Assert.Throws<InvalidProjectFileException>(() => projectCollection.LoadProject(mainProjectPath));
if (string.IsNullOrEmpty(projectValue))
{
logger.AssertLogContains("MSB4102");
}
else
{
#if NETFRAMEWORK
logger.AssertLogContains("MSB4102");
#endif
}
}
private void CreateAndBuildProjectForImportFromExtensionsPath(string extnPathPropertyName, Action<Project, MockLogger> action)
{
string extnDir1 = null, extnDir2 = null, mainProjectPath = null;
try
{
extnDir1 = GetNewExtensionsPathAndCreateFile("extensions1", Path.Combine("foo", "extn.proj"),
GetExtensionTargetsFileContent1(extnPathPropertyName));
extnDir2 = GetNewExtensionsPathAndCreateFile("extensions2", Path.Combine("bar", "extn2.proj"),
GetExtensionTargetsFileContent2(extnPathPropertyName));
mainProjectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("main.proj", GetMainTargetFileContent(extnPathPropertyName));
CreateAndBuildProjectForImportFromExtensionsPath(mainProjectPath, extnPathPropertyName, new string[] { extnDir1, extnDir2 },
null,
action);
}
finally
{
if (extnDir1 != null)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir1, recursive: true);
}
if (extnDir2 != null)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir2, recursive: true);
}
if (mainProjectPath != null)
{
FileUtilities.DeleteNoThrow(mainProjectPath);
}
}
}
private void CreateAndBuildProjectForImportFromExtensionsPath(string mainProjectPath, string extnPathPropertyName, string[] extnDirs, Action<string[]> setExtensionsPath,
Action<Project, MockLogger> action)
{
try
{
using var projColln = GetProjectCollection();
projColln.ResetToolsetsForTests(WriteConfigFileAndGetReader(extnPathPropertyName, extnDirs));
var logger = new MockLogger();
projColln.RegisterLogger(logger);
var project = projColln.LoadProject(mainProjectPath);
action(project, logger);
}
finally
{
if (mainProjectPath != null)
{
FileUtilities.DeleteNoThrow(mainProjectPath);
}
if (extnDirs != null)
{
foreach (var extnDir in extnDirs)
{
FileUtilities.DeleteDirectoryNoThrow(extnDir, recursive: true);
}
}
}
}
private ToolsetConfigurationReader WriteConfigFileAndGetReader(string extnPathPropertyName, params string[] extnDirs)
{
string combinedExtnDirs = extnDirs != null ? String.Join(";", extnDirs) : String.Empty;
ToolsetConfigurationReaderTestHelper.WriteConfigFile(@"
<configuration>
<configSections>
<section name=""msbuildToolsets"" type=""Microsoft.Build.Evaluation.ToolsetConfigurationSection, Microsoft.Build"" />
</configSections>
<msbuildToolsets default=""" + toolsVersionToUse + @""">
<toolset toolsVersion=""" + toolsVersionToUse + @""">
<property name=""MSBuildToolsPath"" value=""."" />
<property name=""MSBuildBinPath"" value=""."" />
<projectImportSearchPaths>
<searchPaths os=""" + NativeMethodsShared.GetOSNameForExtensionsPath() + @""">
<property name=""" + extnPathPropertyName + @""" value=""" + combinedExtnDirs + @""" />
</searchPaths>
</projectImportSearchPaths>
</toolset>
</msbuildToolsets>
</configuration>");
return GetStandardConfigurationReader();
}
private ProjectCollection GetProjectCollection(IDictionary<string, string> globalProperties = null)
{
ProjectCollection projColln;
if (globalProperties == null)
{
#if FEATURE_SYSTEM_CONFIGURATION
projColln = new ProjectCollection();
#else
projColln = new ProjectCollection(ToolsetDefinitionLocations.ConfigurationFile);
#endif
}
else
{
#if FEATURE_SYSTEM_CONFIGURATION
projColln = new ProjectCollection(globalProperties);
#else
projColln = new ProjectCollection(globalProperties, loggers: null, ToolsetDefinitionLocations.ConfigurationFile);
#endif
}
return projColln;
}
private string GetNewExtensionsPathAndCreateFile(string extnDirName, string relativeFilePath, string fileContents)
{
var extnDir = Path.Combine(ObjectModelHelpers.TempProjectDir, extnDirName);
Directory.CreateDirectory(Path.Combine(extnDir, Path.GetDirectoryName(relativeFilePath)));
File.WriteAllText(Path.Combine(extnDir, relativeFilePath), fileContents);
return extnDir;
}
private string GetMainTargetFileContent(string extensionsPathPropertyName = "MSBuildExtensionsPath")
{
string mainTargetsFileContent = @"
<Project>
<Target Name='Main' DependsOnTargets='FromExtn'>
<Message Text='PropertyFromExtn1: $(PropertyFromExtn1)'/>
</Target>
<Import Project='$({0})\foo\extn.proj'/>
</Project>";
return String.Format(mainTargetsFileContent, extensionsPathPropertyName);
}
private string GetExtensionTargetsFileContent1(string extensionsPathPropertyName = "MSBuildExtensionsPath")
{
string extnTargetsFileContent1 = @"
<Project>
<PropertyGroup>
<PropertyFromExtn1>FooBar</PropertyFromExtn1>
</PropertyGroup>
<Target Name='FromExtn'>
<Message Text='Running FromExtn'/>
</Target>
<Import Project='$({0})\bar\extn2.proj'/>
</Project>
";
return String.Format(extnTargetsFileContent1, extensionsPathPropertyName);
}
private string GetExtensionTargetsFileContent2(string extensionsPathPropertyName = "MSBuildExtensionsPath")
{
string extnTargetsFileContent2 = @"
<Project>
<PropertyGroup>
<PropertyFromExtn2>Abc</PropertyFromExtn2>
</PropertyGroup>
<Target Name='FromExtn2'>
<Message Text='Running FromExtn2'/>
</Target>
</Project>
";
return extnTargetsFileContent2;
}
private ToolsetConfigurationReader GetStandardConfigurationReader()
{
using var xmlReader = new ProjectCollection();
return new ToolsetConfigurationReader(xmlReader.EnvironmentProperties, new PropertyDictionary<ProjectPropertyInstance>(), ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest);
}
}
}
|