diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 73d35afcc7b8..40e71f029b54 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -274,6 +274,31 @@ https://github.com/dotnet/deployment-tools b60c95e1ce736630d17e16626c59e3dd85ebae2b + + https://github.com/dotnet/sourcelink + 759f344923a0859f3fae83431d0ba1cc62108118 + + + + https://github.com/dotnet/sourcelink + 759f344923a0859f3fae83431d0ba1cc62108118 + + + https://github.com/dotnet/sourcelink + 759f344923a0859f3fae83431d0ba1cc62108118 + + + https://github.com/dotnet/sourcelink + 759f344923a0859f3fae83431d0ba1cc62108118 + + + https://github.com/dotnet/sourcelink + 759f344923a0859f3fae83431d0ba1cc62108118 + + + https://github.com/dotnet/sourcelink + 759f344923a0859f3fae83431d0ba1cc62108118 + @@ -288,11 +313,6 @@ 17d9eee32f20a6af0ebb620254a22f601d159578 - - https://github.com/dotnet/sourcelink - 759f344923a0859f3fae83431d0ba1cc62108118 - - https://github.com/dotnet/arcade 17d9eee32f20a6af0ebb620254a22f601d159578 diff --git a/eng/Versions.props b/eng/Versions.props index 7ed0fea50a17..59b314468abc 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -174,6 +174,15 @@ 1.0.0-beta.23206.1 + + + 8.0.0-beta.23210.3 + 8.0.0-beta.23210.3 + 8.0.0-beta.23210.3 + 8.0.0-beta.23210.3 + 8.0.0-beta.23210.3 + 8.0.0-beta.23210.3 + true diff --git a/src/Assets/TestProjects/SourceLinkTestApp/Directory.Build.props b/src/Assets/TestProjects/SourceLinkTestApp/Directory.Build.props new file mode 100644 index 000000000000..c1df2220ddc6 --- /dev/null +++ b/src/Assets/TestProjects/SourceLinkTestApp/Directory.Build.props @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/Assets/TestProjects/SourceLinkTestApp/Directory.Build.targets b/src/Assets/TestProjects/SourceLinkTestApp/Directory.Build.targets new file mode 100644 index 000000000000..c1df2220ddc6 --- /dev/null +++ b/src/Assets/TestProjects/SourceLinkTestApp/Directory.Build.targets @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/Assets/TestProjects/SourceLinkTestApp/Program.cs b/src/Assets/TestProjects/SourceLinkTestApp/Program.cs new file mode 100644 index 000000000000..67c7486af97f --- /dev/null +++ b/src/Assets/TestProjects/SourceLinkTestApp/Program.cs @@ -0,0 +1,12 @@ +// 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; + +class Program +{ + public static void Main() + { + Console.WriteLine("Hello World!"); + } +} diff --git a/src/Assets/TestProjects/SourceLinkTestApp/SourceLinkTestApp.csproj b/src/Assets/TestProjects/SourceLinkTestApp/SourceLinkTestApp.csproj new file mode 100644 index 000000000000..213a534d1e60 --- /dev/null +++ b/src/Assets/TestProjects/SourceLinkTestApp/SourceLinkTestApp.csproj @@ -0,0 +1,7 @@ + + + true + $(CurrentTargetFramework) + Exe + + diff --git a/src/Layout/redist/targets/BundledSdks.targets b/src/Layout/redist/targets/BundledSdks.targets index 4ce963aa7eaa..2888625dad7f 100644 --- a/src/Layout/redist/targets/BundledSdks.targets +++ b/src/Layout/redist/targets/BundledSdks.targets @@ -4,5 +4,12 @@ + + + + + + + diff --git a/src/Layout/redist/targets/RestoreDependency.proj b/src/Layout/redist/targets/RestoreDependency.proj index 671413e577f6..bb9d05098845 100644 --- a/src/Layout/redist/targets/RestoreDependency.proj +++ b/src/Layout/redist/targets/RestoreDependency.proj @@ -1,4 +1,4 @@ - + @@ -24,24 +23,28 @@ - + + Condition="!Exists('$(DependencyNuPkgPath)$(DependencyPackageName.ToLower()).nuspec')"> + + + - $(NuGetPackageRoot)/$(DependencyPackageName.ToLower())/$(DependencyPackageVersion.ToLower()) + $(NuGetPackageRoot)$(DependencyPackageName.ToLower())\$(DependencyPackageVersion.ToLower())\ diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.CrossTargeting.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.CrossTargeting.targets index 350beee8d03a..b6affa9c19c9 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.CrossTargeting.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.CrossTargeting.targets @@ -12,6 +12,7 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + + <_SourceLinkSdkSubDir>build + <_SourceLinkSdkSubDir Condition="'$(IsCrossTargetingBuild)' == 'true'">buildMultiTargeting + + + true + + + + + + + + + + + + diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.SourceLink.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.SourceLink.targets new file mode 100644 index 000000000000..42e3019c34a5 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.SourceLink.targets @@ -0,0 +1,29 @@ + + + + + + + + + true + + + + + + + + + + diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.props b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.props index 2bb849977aba..5f4c342792c9 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.props +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.props @@ -157,6 +157,8 @@ Copyright (c) .NET Foundation. All rights reserved. + + diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets index 900ceef00e8b..b5b3bf52e1d7 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets @@ -1207,6 +1207,7 @@ Copyright (c) .NET Foundation. All rights reserved. + diff --git a/src/Tests/Microsoft.NET.Build.Tests/Microsoft.NET.Build.Tests.csproj b/src/Tests/Microsoft.NET.Build.Tests/Microsoft.NET.Build.Tests.csproj index b536cb66226c..9ea2a9b2e710 100644 --- a/src/Tests/Microsoft.NET.Build.Tests/Microsoft.NET.Build.Tests.csproj +++ b/src/Tests/Microsoft.NET.Build.Tests/Microsoft.NET.Build.Tests.csproj @@ -4,6 +4,7 @@ false Tests\$(MSBuildProjectName) + true diff --git a/src/Tests/Microsoft.NET.Build.Tests/SourceLinkTests.cs b/src/Tests/Microsoft.NET.Build.Tests/SourceLinkTests.cs new file mode 100644 index 000000000000..3188f9fd7499 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Tests/SourceLinkTests.cs @@ -0,0 +1,321 @@ +// 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.Linq; +using System.Reflection.Metadata; +using System.Text; +using System.Xml.Linq; +using FluentAssertions; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Commands; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.NET.Build.Tests +{ + public class SourceLinkTests : SdkTest + { + private static readonly Guid s_embeddedSourceKindGuid = new("0E8A571B-6926-466E-B4AD-8AB04611F5FE"); + + public SourceLinkTests(ITestOutputHelper log) + : base(log) + { + } + + private void CreateGitFiles(string repoDir, string originUrl, string commitSha = "1200000000000000000000000000000000000000") + { + var gitDir = Path.Combine(repoDir, ".git"); + var headsDir = Path.Combine(gitDir, "refs", "heads"); + + Directory.CreateDirectory(gitDir); + File.WriteAllText(Path.Combine(gitDir, "HEAD"), "ref: refs/heads/master"); + Directory.CreateDirectory(headsDir); + + if (commitSha != null) + { + File.WriteAllText(Path.Combine(headsDir, "master"), commitSha); + } + + if (originUrl != null) + { + File.WriteAllText(Path.Combine(gitDir, "config"), $""" + [remote "origin"] + url = {originUrl} + """); + } + + File.WriteAllText(Path.Combine(repoDir, ".gitignore"), """ + [Bb]in/ + [Oo]bj/ + """); + } + + private unsafe void ValidatePdb(string pdbPath, bool expectedEmbeddedSources) + { + // Validates that *.AssemblyAttributes.cs file is embedded in the PDB. + + var pdb = File.ReadAllBytes(pdbPath); + fixed (byte* pdbPtr = pdb) + { + var mdReader = new MetadataReader(pdbPtr, pdb.Length); + var attrDocHandle = mdReader.Documents.Single(h => mdReader.GetString(mdReader.GetDocument(h).Name).EndsWith(".AssemblyAttributes.cs")); + var cdis = mdReader.GetCustomDebugInformation(attrDocHandle); + + Assert.Equal(expectedEmbeddedSources, cdis.Any(h => mdReader.GetGuid(mdReader.GetCustomDebugInformation(h).Kind) == s_embeddedSourceKindGuid)); + } + } + + private static TestAsset Multitarget(TestAsset testAsset, string targetFrameworks) + => testAsset.WithProjectChanges(p => + { + var tfmNode = p.Root.Descendants().Single(e => e.Name.LocalName == "TargetFramework"); + tfmNode.Name = p.Root.Name.Namespace + "TargetFrameworks"; + tfmNode.Value = targetFrameworks; + }); + + private static TestAsset WithProperties(TestAsset testAsset, params (string key, string value)[] properties) + => testAsset.WithProjectChanges(p => + { + var ns = p.Root.Name.Namespace; + var propertyGroup = p.Root.Descendants(ns + "PropertyGroup").First(); + + foreach (var (key, value) in properties) + { + propertyGroup.Add(new XElement(ns + key, value)); + } + }); + + private static TestAsset WithItems(TestAsset testAsset, params (string key, XAttribute[] attributes)[] items) + => testAsset.WithProjectChanges(p => + { + var ns = p.Root.Name.Namespace; + + var itemGroup = new XElement(ns + "ItemGroup"); + p.Root.Add(itemGroup); + + foreach (var (key, attributes) in items) + { + itemGroup.Add(new XElement(ns + key, (object[])attributes)); + } + }); + + [Fact] + public void WithNoGitMetadata() + { + // We need to copy the test project to a directory outside of the SDK repo, + // otherwise we would find .git directory in the SDK repo root. + + var testAsset = _testAssetsManager + .CopyTestAsset("SourceLinkTestApp", testDestinationDirectory: Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())) + .WithSource(); + + var buildCommand = new BuildCommand(testAsset); + buildCommand.Execute().Should().Pass(); + + var intermediateDir = buildCommand.GetIntermediateDirectory(); + intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json"); + } + + /// + /// When creating a new repository locally we want the build to work and not report warnings even before the remote is set. + /// + [Fact] + public void WithNoRemoteNoCommit() + { + var testAsset = _testAssetsManager + .CopyTestAsset("SourceLinkTestApp") + .WithSource(); + + CreateGitFiles(testAsset.Path, originUrl: null, commitSha: null); + + var buildCommand = new BuildCommand(testAsset); + buildCommand.Execute().Should().NotHaveStdOutContaining("warning"); + + var intermediateDir = buildCommand.GetIntermediateDirectory(); + intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json"); + } + + /// + /// When creating a new repository locally we want the build to work and not report warnings even before the remote is set. + /// + [Fact] + public void WithNoRemote() + { + var testAsset = _testAssetsManager + .CopyTestAsset("SourceLinkTestApp") + .WithSource(); + + CreateGitFiles(testAsset.Path, originUrl: null); + + var buildCommand = new BuildCommand(testAsset); + buildCommand.Execute().Should().NotHaveStdOutContaining("warning"); + + var intermediateDir = buildCommand.GetIntermediateDirectory(); + intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json"); + } + + [Fact] + public void WithRemoteOrigin_UnknownDomain() + { + var testAsset = _testAssetsManager + .CopyTestAsset("SourceLinkTestApp") + .WithSource(); + + CreateGitFiles(testAsset.Path, originUrl: "https://contoso.com"); + + var buildCommand = new BuildCommand(testAsset) + { + WorkingDirectory = testAsset.Path + }; + + buildCommand.Execute().Should().Pass(); + + var intermediateDir = buildCommand.GetIntermediateDirectory(); + intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json"); + } + + [Theory] + [InlineData("https://github.com/org/repo", "https://raw.githubusercontent.com/org/repo/1200000000000000000000000000000000000000/*", true)] + [InlineData("https://github.com/org/repo", "https://raw.githubusercontent.com/org/repo/1200000000000000000000000000000000000000/*", false)] + [InlineData("https://gitlab.com/org/repo", "https://gitlab.com/org/repo/-/raw/1200000000000000000000000000000000000000/*")] + [InlineData("https://bitbucket.org/org/repo", "https://api.bitbucket.org/2.0/repositories/org/repo/src/1200000000000000000000000000000000000000/*")] + [InlineData("https://test.visualstudio.com/org/_git/repo", "https://test.visualstudio.com/org/_apis/git/repositories/repo/items?api-version=1.0&versionType=commit&version=1200000000000000000000000000000000000000&path=/*")] + public void WithRemoteOrigin_KnownDomain(string origin, string expectedLink, bool multitarget = false) + { + string targetFrameworks = ToolsetInfo.CurrentTargetFramework + (multitarget ? ";netstandard2.0" : ""); + + var testAsset = _testAssetsManager + .CopyTestAsset("SourceLinkTestApp", identifier: origin) + .WithSource(); + + if (multitarget) + { + testAsset = Multitarget(testAsset, targetFrameworks); + } + + CreateGitFiles(testAsset.Path, origin); + + var buildCommand = new BuildCommand(testAsset) + { + WorkingDirectory = testAsset.Path + }; + + buildCommand.Execute().Should().Pass(); + + foreach (var targetFramework in targetFrameworks.Split(';')) + { + var intermediateDir = buildCommand.GetIntermediateDirectory(targetFramework: targetFramework); + var sourceLinkFilePath = Path.Combine(intermediateDir.FullName, "SourceLinkTestApp.sourcelink.json"); + var actualContent = File.ReadAllText(sourceLinkFilePath, Encoding.UTF8); + var expectedPattern = Path.Combine(testAsset.Path, "*").Replace("\\", "\\\\"); + + Assert.Equal($$$"""{"documents":{"{{{expectedPattern}}}":"{{{expectedLink}}}"}}""", actualContent); + + ValidatePdb(Path.Combine(intermediateDir.FullName, "SourceLinkTestApp.pdb"), expectedEmbeddedSources: true); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SuppressImplicitGitSourceLink_SetExplicitly(bool multitarget) + { + string targetFrameworks = ToolsetInfo.CurrentTargetFramework + (multitarget ? ";netstandard2.0" : ""); + + var testAsset = _testAssetsManager + .CopyTestAsset("SourceLinkTestApp", identifier: multitarget.ToString()) + .WithSource(); + + testAsset = WithProperties(testAsset, ("SuppressImplicitGitSourceLink", "true")); + + if (multitarget) + { + testAsset = Multitarget(testAsset, targetFrameworks); + } + + CreateGitFiles(testAsset.Path, "https://github.com/org/repo"); + + var buildCommand = new BuildCommand(testAsset) + { + WorkingDirectory = testAsset.Path + }; + + buildCommand.Execute().Should().Pass(); + + foreach (var targetFramework in targetFrameworks.Split(';')) + { + var intermediateDir = buildCommand.GetIntermediateDirectory(targetFramework: targetFramework); + intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json"); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SuppressImplicitGitSourceLink_ExplicitPackage(bool multitarget) + { + string targetFrameworks = ToolsetInfo.CurrentTargetFramework + (multitarget ? ";netstandard2.0" : ""); + + var testAsset = _testAssetsManager + .CopyTestAsset("SourceLinkTestApp", identifier: multitarget.ToString()) + .WithSource(); + + if (multitarget) + { + testAsset = Multitarget(testAsset, targetFrameworks); + } + + testAsset = WithItems(testAsset, ("PackageReference", new[] { new XAttribute("Include", "Microsoft.SourceLink.GitHub"), new XAttribute("Version", "1.0.0") })); + + CreateGitFiles(testAsset.Path, "https://github.com/org/repo"); + + var buildCommand = new BuildCommand(testAsset) + { + WorkingDirectory = testAsset.Path + }; + + buildCommand.Execute().Should().Pass(); + + foreach (var targetFramework in targetFrameworks.Split(';')) + { + var intermediateDir = buildCommand.GetIntermediateDirectory(targetFramework: targetFramework); + intermediateDir.Should().HaveFile("SourceLinkTestApp.sourcelink.json"); + + // EmbedUntrackedSources is not set by default by SourceLink v1.0.0 package: + ValidatePdb(Path.Combine(intermediateDir.FullName, "SourceLinkTestApp.pdb"), expectedEmbeddedSources: false); + } + } + + [FullMSBuildOnlyFact] + public void Cpp() + { + var testAsset = _testAssetsManager + .CopyTestAsset("NetCoreCsharpAppReferenceCppCliLib") + .WithSource(); + + testAsset = WithProperties(testAsset, ("EnableManagedPackageReferenceSupport", "true")); + + CreateGitFiles(testAsset.Path, "https://github.com/org/repo"); + + var buildCommand = new BuildCommand(testAsset, "NETCoreCppCliTest") + { + WorkingDirectory = testAsset.Path + }; + + buildCommand.Execute("-p:Platform=x64").Should().Pass(); + + var outputDir = Path.Combine(testAsset.Path, "NETCoreCppCliTest", "x64", "Debug"); + var sourceLinkFilePath = Path.Combine(outputDir, "NETCoreCppCliTest.sourcelink.json"); + var actualContent = File.ReadAllText(sourceLinkFilePath, Encoding.UTF8); + var expectedPattern = Path.Combine(testAsset.Path, "*").Replace("\\", "\\\\"); + var expectedSourceLink = $$$"""{"documents":{"{{{expectedPattern}}}":"https://raw.githubusercontent.com/org/repo/1200000000000000000000000000000000000000/*"}}"""; + Assert.Equal(expectedSourceLink, actualContent); + + var pdbText = File.ReadAllText(Path.Combine(outputDir, "NETCoreCppCliTest.pdb"), Encoding.UTF8); + Assert.Contains(expectedSourceLink, pdbText); + } + } +} diff --git a/src/Tests/Microsoft.NET.TestFramework/TestAssetsManager.cs b/src/Tests/Microsoft.NET.TestFramework/TestAssetsManager.cs index e239e27c1d13..64d5ee549b45 100644 --- a/src/Tests/Microsoft.NET.TestFramework/TestAssetsManager.cs +++ b/src/Tests/Microsoft.NET.TestFramework/TestAssetsManager.cs @@ -43,13 +43,13 @@ public TestAsset CopyTestAsset( [CallerFilePath] string callerFilePath = null, string identifier = "", string testAssetSubdirectory = "", + string testDestinationDirectory = null, bool allowCopyIfPresent = false) { var testProjectDirectory = GetAndValidateTestProjectDirectory(testProjectName, testAssetSubdirectory); var fileName = Path.GetFileNameWithoutExtension(callerFilePath); - var testDestinationDirectory = - GetTestDestinationDirectoryPath(testProjectName, callingMethod + "_" + fileName, identifier, allowCopyIfPresent); + testDestinationDirectory ??= GetTestDestinationDirectoryPath(testProjectName, callingMethod + "_" + fileName, identifier, allowCopyIfPresent); TestDestinationDirectories.Add(testDestinationDirectory); var testAsset = new TestAsset(testProjectDirectory, testDestinationDirectory, TestContext.Current.SdkVersion, Log);