ArtifactsOutputPathTests.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using System.Runtime.CompilerServices;
  4. namespace Microsoft.NET.Build.Tests
  5. {
  6. using System.Runtime.InteropServices;
  7. using ArtifactsTestExtensions;
  8. public class ArtifactsOutputPathTests : SdkTest
  9. {
  10. public ArtifactsOutputPathTests(ITestOutputHelper log) : base(log)
  11. {
  12. }
  13. (List<TestProject> testProjects, TestAsset testAsset) GetTestProjects(bool putArtifactsInProjectFolder = false, [CallerMemberName] string callingMethod = "")
  14. {
  15. var testProject1 = new TestProject()
  16. {
  17. Name = "App1",
  18. IsExe = true
  19. };
  20. var testProject2 = new TestProject()
  21. {
  22. Name = "App2",
  23. IsExe = true
  24. };
  25. var testLibraryProject = new TestProject()
  26. {
  27. Name = "Library",
  28. };
  29. testProject1.ReferencedProjects.Add(testLibraryProject);
  30. testProject2.ReferencedProjects.Add(testLibraryProject);
  31. List<TestProject> testProjects = new() { testProject1, testProject2, testLibraryProject };
  32. foreach (var testProject in testProjects)
  33. {
  34. testProject.UseArtifactsOutput = true;
  35. }
  36. var testAsset = _testAssetsManager.CreateTestProjects(testProjects, callingMethod: callingMethod, identifier: putArtifactsInProjectFolder.ToString());
  37. if (putArtifactsInProjectFolder)
  38. {
  39. File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"),
  40. """
  41. <Project>
  42. <PropertyGroup>
  43. <ArtifactsPath>$(MSBuildProjectDirectory)\artifacts</ArtifactsPath>
  44. <IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
  45. </PropertyGroup>
  46. </Project>
  47. """);
  48. }
  49. else
  50. {
  51. File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"),
  52. """
  53. <Project>
  54. <PropertyGroup>
  55. <UseArtifactsOutput>true</UseArtifactsOutput>
  56. </PropertyGroup>
  57. </Project>
  58. """);
  59. }
  60. return (testProjects, testAsset);
  61. }
  62. [Fact]
  63. public void ItUsesArtifactsOutputPathForBuild()
  64. {
  65. var (testProjects, testAsset) = GetTestProjects();
  66. new DotnetCommand(Log, "build")
  67. .WithWorkingDirectory(testAsset.Path)
  68. .Execute()
  69. .Should()
  70. .Pass();
  71. ValidateIntermediatePaths(testAsset, testProjects);
  72. foreach (var testProject in testProjects)
  73. {
  74. OutputPathCalculator outputPathCalculator = OutputPathCalculator.FromProject(Path.Combine(testAsset.Path, testProject.Name), testProject);
  75. new FileInfo(Path.Combine(outputPathCalculator.GetOutputDirectory(), testProject.Name + ".dll"))
  76. .Should()
  77. .Exist();
  78. }
  79. }
  80. [Fact]
  81. public void ItUsesArtifactsOutputPathForPublish()
  82. {
  83. var (testProjects, testAsset) = GetTestProjects();
  84. new DotnetCommand(Log, "publish")
  85. .WithWorkingDirectory(testAsset.Path)
  86. .Execute()
  87. .Should()
  88. .Pass();
  89. ValidateIntermediatePaths(testAsset, testProjects, "release");
  90. foreach (var testProject in testProjects)
  91. {
  92. OutputPathCalculator outputPathCalculator = OutputPathCalculator.FromProject(Path.Combine(testAsset.Path, testProject.Name), testProject);
  93. new FileInfo(Path.Combine(outputPathCalculator.GetOutputDirectory(configuration: "release"), testProject.Name + ".dll"))
  94. .Should()
  95. .Exist();
  96. new FileInfo(Path.Combine(outputPathCalculator.GetPublishDirectory(configuration: "release"), testProject.Name + ".dll"))
  97. .Should()
  98. .Exist();
  99. }
  100. }
  101. [Fact]
  102. public void ItUseArtifactsOutputPathForPack()
  103. {
  104. var (testProjects, testAsset) = GetTestProjects();
  105. new DotnetCommand(Log, "pack")
  106. .WithWorkingDirectory(testAsset.Path)
  107. .Execute()
  108. .Should()
  109. .Pass();
  110. ValidateIntermediatePaths(testAsset, testProjects, "release");
  111. foreach (var testProject in testProjects)
  112. {
  113. OutputPathCalculator outputPathCalculator = OutputPathCalculator.FromProject(Path.Combine(testAsset.Path, testProject.Name), testProject);
  114. new FileInfo(Path.Combine(outputPathCalculator.GetOutputDirectory(configuration: "release"), testProject.Name + ".dll"))
  115. .Should()
  116. .Exist();
  117. new FileInfo(Path.Combine(outputPathCalculator.GetPackageDirectory(configuration: "release"), testProject.Name + ".1.0.0.nupkg"))
  118. .Should()
  119. .Exist();
  120. }
  121. }
  122. void ValidateIntermediatePaths(TestAsset testAsset, IEnumerable<TestProject> testProjects, string configuration = "debug")
  123. {
  124. foreach (var testProject in testProjects)
  125. {
  126. new DirectoryInfo(Path.Combine(testAsset.TestRoot, testProject.Name))
  127. .Should()
  128. .NotHaveSubDirectories();
  129. new DirectoryInfo(Path.Combine(testAsset.TestRoot, "artifacts", "obj", testProject.Name, configuration))
  130. .Should()
  131. .Exist();
  132. }
  133. }
  134. [Fact]
  135. public void ArtifactsPathCanBeInProjectFolder()
  136. {
  137. var (testProjects, testAsset) = GetTestProjects(putArtifactsInProjectFolder: true);
  138. new DotnetCommand(Log, "build")
  139. .WithWorkingDirectory(testAsset.Path)
  140. .Execute()
  141. .Should()
  142. .Pass();
  143. foreach (var testProject in testProjects)
  144. {
  145. var outputPathCalculator = OutputPathCalculator.FromProject(testAsset.Path, testProject);
  146. outputPathCalculator.IncludeProjectNameInArtifactsPaths = false;
  147. outputPathCalculator.ArtifactsPath = Path.Combine(testAsset.Path, testProject.Name, "artifacts");
  148. new DirectoryInfo(outputPathCalculator.GetIntermediateDirectory())
  149. .Should()
  150. .Exist();
  151. new FileInfo(Path.Combine(outputPathCalculator.GetOutputDirectory(), testProject.Name + ".dll"))
  152. .Should()
  153. .Exist();
  154. }
  155. }
  156. [Fact]
  157. public void ProjectsCanSwitchOutputFormats()
  158. {
  159. var testProject = new TestProject()
  160. {
  161. IsExe = true,
  162. };
  163. var testAsset = _testAssetsManager.CreateTestProject(testProject);
  164. // Build without artifacts format
  165. new BuildCommand(testAsset)
  166. .Execute()
  167. .Should()
  168. .Pass();
  169. new DirectoryInfo(OutputPathCalculator.FromProject(testAsset.Path, testProject).GetOutputDirectory())
  170. .Should()
  171. .Exist();
  172. // Now add a Directory.Build.props file setting UseArtifactsOutput to true
  173. File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"), """
  174. <Project>
  175. <PropertyGroup>
  176. <UseArtifactsOutput>true</UseArtifactsOutput>
  177. </PropertyGroup>
  178. </Project>
  179. """);
  180. new BuildCommand(testAsset)
  181. .Execute()
  182. .Should()
  183. .Pass();
  184. new DirectoryInfo(OutputPathCalculator.FromProject(testAsset.Path, testProject).GetOutputDirectory())
  185. .Should()
  186. .Exist();
  187. // Now go back to not using artifacts output format
  188. File.Delete(Path.Combine(testAsset.Path, "Directory.Build.props"));
  189. new BuildCommand(testAsset)
  190. .Execute()
  191. .Should()
  192. .Pass();
  193. }
  194. [Fact]
  195. public void ProjectsCanCustomizeOutputPathBasedOnTargetFramework()
  196. {
  197. var testProject = new TestProject("CustomizeArtifactsPath")
  198. {
  199. IsExe = true,
  200. TargetFrameworks = "net7.0;net8.0;netstandard2.0"
  201. };
  202. var testAsset = _testAssetsManager.CreateTestProject(testProject);
  203. File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"), """
  204. <Project>
  205. <PropertyGroup>
  206. <UseArtifactsOutput>true</UseArtifactsOutput>
  207. <AfterTargetFrameworkInferenceTargets>$(MSBuildThisFileDirectory)\Directory.AfterTargetFrameworkInference.targets</AfterTargetFrameworkInferenceTargets>
  208. </PropertyGroup>
  209. </Project>
  210. """);
  211. File.WriteAllText(Path.Combine(testAsset.Path, "Directory.AfterTargetFrameworkInference.targets"), """
  212. <Project>
  213. <PropertyGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
  214. <ArtifactsPivots Condition="'$(_TargetFrameworkVersionWithoutV)' == '8.0'">NET8_$(Configuration)</ArtifactsPivots>
  215. <ArtifactsPivots Condition="'$(_TargetFrameworkVersionWithoutV)' == '7.0'">NET7_$(Configuration)</ArtifactsPivots>
  216. </PropertyGroup>
  217. </Project>
  218. """);
  219. new BuildCommand(testAsset)
  220. .Execute()
  221. .Should()
  222. .Pass();
  223. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "NET8_Debug")).Should().Exist();
  224. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "NET7_Debug")).Should().Exist();
  225. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "debug_netstandard2.0")).Should().Exist();
  226. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "debug_net8.0")).Should().NotExist();
  227. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "bin", testProject.Name, "debug_net7.0")).Should().NotExist();
  228. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "NET8_Debug")).Should().Exist();
  229. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "NET7_Debug")).Should().Exist();
  230. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "debug_netstandard2.0")).Should().Exist();
  231. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "debug_net8.0")).Should().NotExist();
  232. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "obj", testProject.Name, "debug_net7.0")).Should().NotExist();
  233. foreach (var targetFramework in testProject.TargetFrameworks.Split(';'))
  234. {
  235. new DotnetPublishCommand(Log, "-f", targetFramework)
  236. .WithWorkingDirectory(Path.Combine(testAsset.Path, testProject.Name))
  237. .Execute()
  238. .Should()
  239. .Pass();
  240. }
  241. // Note that publish defaults to release configuration for .NET 8 but not prior TargetFrameworks
  242. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "publish", testProject.Name, "NET8_Release")).Should().Exist();
  243. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "publish", testProject.Name, "NET7_Debug")).Should().Exist();
  244. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "publish", testProject.Name, "debug_netstandard2.0")).Should().Exist();
  245. new DotnetPackCommand(Log)
  246. .WithWorkingDirectory(Path.Combine(testAsset.Path, testProject.Name))
  247. .Execute()
  248. .Should()
  249. .Pass();
  250. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "package", "release")).Should().Exist();
  251. new FileInfo(Path.Combine(testAsset.Path, "artifacts", "package", "release", testProject.Name + ".1.0.0.nupkg")).Should().Exist();
  252. }
  253. TestAsset CreateCustomizedTestProject(string propertyName, string propertyValue, [CallerMemberName] string callingMethod = "")
  254. {
  255. var testProject = new TestProject("App")
  256. {
  257. IsExe = true,
  258. UseArtifactsOutput = true
  259. };
  260. var testAsset = _testAssetsManager.CreateTestProjects(new[] { testProject }, callingMethod: callingMethod);
  261. File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"),
  262. $"""
  263. <Project>
  264. <PropertyGroup>
  265. <UseArtifactsOutput>true</UseArtifactsOutput>
  266. <{propertyName}>{propertyValue}</{propertyName}>
  267. </PropertyGroup>
  268. </Project>
  269. """);
  270. return testAsset;
  271. }
  272. [Fact]
  273. public void ArtifactsPathCanBeSet()
  274. {
  275. var artifactsFolder = _testAssetsManager.CreateTestDirectory(identifier: "ArtifactsPath").Path;
  276. var testAsset = CreateCustomizedTestProject("ArtifactsPath", artifactsFolder);
  277. new DotnetBuildCommand(testAsset)
  278. .Execute()
  279. .Should()
  280. .Pass();
  281. // If ArtifactsPath is set, even in the project file itself, we still include the project name in the path,
  282. // as the path used is likely to be shared between multiple projects
  283. new FileInfo(Path.Combine(artifactsFolder, "bin", "App", "debug", "App.dll"))
  284. .Should()
  285. .Exist();
  286. }
  287. [Fact]
  288. public void BinOutputNameCanBeSet()
  289. {
  290. var testAsset = CreateCustomizedTestProject("ArtifactsBinOutputName", "binaries");
  291. new DotnetBuildCommand(testAsset)
  292. .Execute()
  293. .Should()
  294. .Pass();
  295. new FileInfo(Path.Combine(testAsset.Path, "artifacts", "binaries", "App", "debug", "App.dll"))
  296. .Should()
  297. .Exist();
  298. }
  299. [Fact]
  300. public void PublishOutputNameCanBeSet()
  301. {
  302. var testAsset = CreateCustomizedTestProject("ArtifactsPublishOutputName", "published_app");
  303. new DotnetPublishCommand(Log)
  304. .WithWorkingDirectory(testAsset.Path)
  305. .Execute()
  306. .Should()
  307. .Pass();
  308. new FileInfo(Path.Combine(testAsset.Path, "artifacts", "published_app", "App", "release", "App.dll"))
  309. .Should()
  310. .Exist();
  311. }
  312. [Fact]
  313. public void PackageOutputNameCanBeSet()
  314. {
  315. var testAsset = CreateCustomizedTestProject("ArtifactsPackageOutputName", "package_output");
  316. new DotnetPackCommand(Log)
  317. .WithWorkingDirectory(testAsset.Path)
  318. .Execute()
  319. .Should()
  320. .Pass();
  321. new FileInfo(Path.Combine(testAsset.Path, "artifacts", "package_output", "release", "App.1.0.0.nupkg"))
  322. .Should()
  323. .Exist();
  324. }
  325. [Fact]
  326. public void ProjectNameCanBeSet()
  327. {
  328. var testAsset = CreateCustomizedTestProject("ArtifactsProjectName", "Apps\\MyApp");
  329. new DotnetBuildCommand(Log)
  330. .WithWorkingDirectory(testAsset.Path)
  331. .Execute()
  332. .Should()
  333. .Pass();
  334. new FileInfo(Path.Combine(testAsset.Path, "artifacts", "bin", "Apps", "MyApp", "debug", "App.dll"))
  335. .Should()
  336. .Exist();
  337. }
  338. [Fact]
  339. public void PackageValidationSucceeds()
  340. {
  341. var testProject = new TestProject()
  342. {
  343. TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net7.0"
  344. };
  345. testProject.AdditionalProperties["EnablePackageValidation"] = "True";
  346. testProject.UseArtifactsOutput = true;
  347. var testAsset = _testAssetsManager.CreateTestProject(testProject);
  348. File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"),
  349. $"""
  350. <Project>
  351. <PropertyGroup>
  352. <UseArtifactsOutput>true</UseArtifactsOutput>
  353. </PropertyGroup>
  354. </Project>
  355. """);
  356. new DotnetPackCommand(Log)
  357. .WithWorkingDirectory(Path.Combine(testAsset.TestRoot, testProject.Name))
  358. .Execute()
  359. .Should()
  360. .Pass();
  361. }
  362. [Fact]
  363. public void ItErrorsIfArtifactsPathIsSetInProject()
  364. {
  365. var testProject = new TestProject();
  366. testProject.AdditionalProperties["ArtifactsPath"] = "$(MSBuildThisFileDirectory)\\..\\artifacts";
  367. var testAsset = _testAssetsManager.CreateTestProject(testProject);
  368. new BuildCommand(testAsset)
  369. .Execute()
  370. .Should()
  371. .Fail()
  372. .And
  373. .HaveStdOutContaining("NETSDK1199");
  374. new DirectoryInfo(Path.Combine(testAsset.TestRoot, "artifacts"))
  375. .Should()
  376. .NotExist();
  377. }
  378. [Fact]
  379. public void ItErrorsIfUseArtifactsOutputIsSetInProject()
  380. {
  381. var testProject = new TestProject();
  382. testProject.AdditionalProperties["UseArtifactsOutput"] = "true";
  383. var testAsset = _testAssetsManager.CreateTestProject(testProject);
  384. new BuildCommand(testAsset)
  385. .Execute()
  386. .Should()
  387. .Fail()
  388. .And
  389. .HaveStdOutContaining("NETSDK1199");
  390. new DirectoryInfo(Path.Combine(testAsset.TestRoot, testProject.Name, "artifacts"))
  391. .Should()
  392. .NotExist();
  393. }
  394. [Fact]
  395. public void ItErrorsIfUseArtifactsOutputIsSetAndThereIsNoDirectoryBuildProps()
  396. {
  397. var testProject = new TestProject();
  398. var testAsset = _testAssetsManager.CreateTestProject(testProject);
  399. new BuildCommand(testAsset)
  400. .DisableDirectoryBuildProps()
  401. .Execute("/p:UseArtifactsOutput=true")
  402. .Should()
  403. .Fail()
  404. .And
  405. .HaveStdOutContaining("NETSDK1200");
  406. }
  407. [Fact(Skip = "https://github.com/dotnet/sdk/issues/40160")]
  408. public void ItCanBuildWithMicrosoftBuildArtifactsSdk()
  409. {
  410. var testAsset = _testAssetsManager.CopyTestAsset("ArtifactsSdkTest")
  411. .WithSource();
  412. new DotnetBuildCommand(testAsset)
  413. .Execute()
  414. .Should()
  415. .Pass();
  416. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "MSBuildSdk", ToolsetInfo.CurrentTargetFramework))
  417. .Should()
  418. .OnlyHaveFiles(new[] { "MSBuildSdk.dll" });
  419. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  420. {
  421. // Microsoft.Build.Artifacts doesn't appear to copy the (extensionless) executable to the artifacts folder on non-Windows platforms
  422. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "PackageReference", ToolsetInfo.CurrentTargetFramework))
  423. .Should()
  424. .OnlyHaveFiles(new[] { "PackageReference.dll", $"PackageReference{EnvironmentInfo.ExecutableExtension}" });
  425. }
  426. else
  427. {
  428. new DirectoryInfo(Path.Combine(testAsset.Path, "artifacts", "PackageReference", ToolsetInfo.CurrentTargetFramework))
  429. .Should()
  430. .OnlyHaveFiles(new[] { "PackageReference.dll" });
  431. }
  432. // Verify that default bin and obj folders still exist (which wouldn't be the case if using the .NET SDKs artifacts output functianality
  433. new FileInfo(Path.Combine(testAsset.Path, "MSBuildSdk", "bin", "Debug", ToolsetInfo.CurrentTargetFramework, "MSBuildSdk.dll")).Should().Exist();
  434. new FileInfo(Path.Combine(testAsset.Path, "MSBuildSdk", "obj", "Debug", ToolsetInfo.CurrentTargetFramework, "MSBuildSdk.dll")).Should().Exist();
  435. }
  436. }
  437. namespace ArtifactsTestExtensions
  438. {
  439. static class Extensions
  440. {
  441. public static TestCommand DisableDirectoryBuildProps(this TestCommand command)
  442. {
  443. // There is an empty Directory.Build.props file in the test execution root, to stop other files further up in the repo from
  444. // impacting the tests. So if a project set UseArtifactsOutput to true, the logic would find that file and put the output
  445. // in that folder. To simulate the situation where there is no Directory.Build.props, we turn it off via an environment
  446. // variable.
  447. return command.WithEnvironmentVariable("ImportDirectoryBuildProps", "false");
  448. }
  449. }
  450. }
  451. }