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