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);