diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index e7dfa7f6543f..8c8ebf3cb21a 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -270,6 +270,36 @@
https://github.com/dotnet/deployment-tools
b60c95e1ce736630d17e16626c59e3dd85ebae2b
+
+ https://github.com/dotnet/sourcelink
+ 31a772fb8b2e3430e91766cdb169fba379f0d013
+
+
+
+ https://github.com/dotnet/sourcelink
+ 31a772fb8b2e3430e91766cdb169fba379f0d013
+
+
+
+ https://github.com/dotnet/sourcelink
+ 31a772fb8b2e3430e91766cdb169fba379f0d013
+
+
+
+ https://github.com/dotnet/sourcelink
+ 31a772fb8b2e3430e91766cdb169fba379f0d013
+
+
+
+ https://github.com/dotnet/sourcelink
+ 31a772fb8b2e3430e91766cdb169fba379f0d013
+
+
+
+ https://github.com/dotnet/sourcelink
+ 31a772fb8b2e3430e91766cdb169fba379f0d013
+
+
@@ -284,9 +314,9 @@
290a3ecc0e3dabcdcafff632ccbf28d42db8061b
-
+
https://github.com/dotnet/sourcelink
- 47edfd68f25fc5a9dd399b6e3f8f20a355f82e1a
+ 31a772fb8b2e3430e91766cdb169fba379f0d013
diff --git a/eng/Versions.props b/eng/Versions.props
index 9e844e044fb7..7ba28dae5b7b 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -175,6 +175,15 @@
1.0.0-beta.23180.1
+
+
+ 1.2.0-beta-23207-01
+ 1.2.0-beta-23207-01
+ 1.2.0-beta-23207-01
+ 1.2.0-beta-23207-01
+ 1.2.0-beta-23207-01
+ 1.2.0-beta-23207-01
+
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..ebc51ad50005
--- /dev/null
+++ b/src/Assets/TestProjects/SourceLinkTestApp/Program.cs
@@ -0,0 +1,6 @@
+// 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;
+
+Console.WriteLine("Hello World!");
\ No newline at end of file
diff --git a/src/Assets/TestProjects/SourceLinkTestApp/SourceLinkTestApp.csproj b/src/Assets/TestProjects/SourceLinkTestApp/SourceLinkTestApp.csproj
new file mode 100644
index 000000000000..e3a5924a1691
--- /dev/null
+++ b/src/Assets/TestProjects/SourceLinkTestApp/SourceLinkTestApp.csproj
@@ -0,0 +1,8 @@
+
+
+ $(MSBuildThisFileDirectory)..\..\..\artifacts\.nuget\packages
+ 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..e3308ce93a93 100644
--- a/src/Layout/redist/targets/RestoreDependency.proj
+++ b/src/Layout/redist/targets/RestoreDependency.proj
@@ -1,4 +1,4 @@
-
+
+
+
$(MSBuildThisFileDirectory)..\tools\
net8.0
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.SourceLink.props b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.SourceLink.props
new file mode 100644
index 000000000000..884051b5dd9a
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.SourceLink.props
@@ -0,0 +1,30 @@
+
+
+
+
+
+ <_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..033a86b990ab
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.SourceLink.targets
@@ -0,0 +1,26 @@
+
+
+
+
+
+ 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/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..3b8d93ec0ce6
--- /dev/null
+++ b/src/Tests/Microsoft.NET.Build.Tests/SourceLinkTests.cs
@@ -0,0 +1,238 @@
+// 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));
+ }
+ }
+
+ [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().HaveStdOutContaining($"warning : Repository '{testAsset.Path}' has no remote.");
+
+ 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().HaveStdOutContaining($"warning : Repository '{testAsset.Path}' has no remote.");
+
+ 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
+ };
+
+ var result = buildCommand.Execute().Should().Pass();
+
+ var intermediateDir = buildCommand.GetIntermediateDirectory();
+ intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json");
+ }
+
+ [Theory]
+ [InlineData("https://github.com/org/repo", "https://github.com/raw/org/repo/1200000000000000000000000000000000000000/*")]
+ [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)
+ {
+ var testAsset = _testAssetsManager
+ .CopyTestAsset("SourceLinkTestApp", identifier: origin)
+ .WithSource();
+
+ CreateGitFiles(testAsset.Path, origin);
+
+ var buildCommand = new BuildCommand(testAsset)
+ {
+ WorkingDirectory = testAsset.Path
+ };
+
+ var result = buildCommand.Execute().Should().Pass();
+
+ var intermediateDir = buildCommand.GetIntermediateDirectory();
+ 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);
+ }
+
+ [Fact]
+ public void SuppressImplicitGitSourceLink_SetExplicitly()
+ {
+ var testAsset = _testAssetsManager
+ .CopyTestAsset("SourceLinkTestApp")
+ .WithSource().WithProjectChanges(p =>
+ {
+ var ns = p.Root.Name.Namespace;
+
+ var propertyGroup = new XElement(ns + "PropertyGroup");
+ p.Root.Add(propertyGroup);
+
+ propertyGroup.Add(new XElement(ns + "SuppressImplicitGitSourceLink", "true"));
+ });
+
+ CreateGitFiles(testAsset.Path, "https://github.com/org/repo");
+
+ var buildCommand = new BuildCommand(testAsset)
+ {
+ WorkingDirectory = testAsset.Path
+ };
+
+ var result = buildCommand.Execute().Should().Pass();
+
+ var intermediateDir = buildCommand.GetIntermediateDirectory();
+ intermediateDir.Should().NotHaveFile("SourceLinkTestApp.sourcelink.json");
+ }
+
+ [Fact]
+ public void SuppressImplicitGitSourceLink_ExplicitPackage()
+ {
+ var testAsset = _testAssetsManager
+ .CopyTestAsset("SourceLinkTestApp")
+ .WithSource()
+ .WithProjectChanges(p =>
+ {
+ var ns = p.Root.Name.Namespace;
+
+ var itemGroup = new XElement(ns + "ItemGroup");
+ p.Root.Add(itemGroup);
+
+ itemGroup.Add(new XElement(ns + "PackageReference",
+ 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
+ };
+
+ var result = buildCommand.Execute().Should().Pass();
+
+ var intermediateDir = buildCommand.GetIntermediateDirectory();
+ 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);
+ }
+ }
+}
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);