diff --git a/src/SourceLink.Common.UnitTests/GenerateSourceLinkFileTests.cs b/src/SourceLink.Common.UnitTests/GenerateSourceLinkFileTests.cs index 923442173d..5424cb5c5a 100644 --- a/src/SourceLink.Common.UnitTests/GenerateSourceLinkFileTests.cs +++ b/src/SourceLink.Common.UnitTests/GenerateSourceLinkFileTests.cs @@ -239,5 +239,26 @@ public void DoesNotRewriteContentIfFileContentIsSame() Assert.Equal(beforeWriteTime, afterWriteTime); } + + [Fact] + public void WriteSourceLinkFileRejectsRelativeOutputFile() + { + var engine = new MockEngine(); + var task = new GenerateSourceLinkFile() + { + BuildEngine = engine, + // OutputFile must be fully qualified: file access must not depend on the shared process + // current directory under the multithreaded task model, so a relative path is rejected. + OutputFile = "sourcelink.json", + SourceRoots = new[] + { + new MockItem(@"/_/", KVP("SourceLinkUrl", "https://raw.githubusercontent.com/repo/*"), KVP("SourceControl", "git")), + }, + }; + + Assert.False(task.Execute()); + Assert.Null(task.SourceLink); + Assert.Contains("ERROR", engine.Log); + } } } diff --git a/src/SourceLink.Common/GenerateSourceLinkFile.cs b/src/SourceLink.Common/GenerateSourceLinkFile.cs index 490be432fc..cd0d8cb937 100644 --- a/src/SourceLink.Common/GenerateSourceLinkFile.cs +++ b/src/SourceLink.Common/GenerateSourceLinkFile.cs @@ -13,6 +13,7 @@ namespace Microsoft.SourceLink.Common { + [MSBuildMultiThreadableTask] public sealed class GenerateSourceLinkFile : Task { [Required, NotNull] @@ -114,6 +115,14 @@ private void WriteSourceLinkFile(string? content) try { + // OutputFile is required to be a fully qualified path. It is set by the SourceLink targets from + // _SourceLinkFilePath, so file access must not (and does not need to) depend on the shared process + // current directory under the multithreaded task model. Reject a path that isn't fully qualified. + if (string.IsNullOrEmpty(OutputFile) || !PathUtilities.IsPathFullyQualified(OutputFile)) + { + throw new ArgumentException($"The path '{OutputFile}' must be fully qualified.", nameof(OutputFile)); + } + if (File.Exists(OutputFile)) { if (content == null) diff --git a/src/SourceLink.Common/build/Microsoft.SourceLink.Common.targets b/src/SourceLink.Common/build/Microsoft.SourceLink.Common.targets index 8d2e91e874..7d9f52823a 100644 --- a/src/SourceLink.Common/build/Microsoft.SourceLink.Common.targets +++ b/src/SourceLink.Common/build/Microsoft.SourceLink.Common.targets @@ -6,7 +6,12 @@ - <_SourceLinkFilePath>$(IntermediateOutputPath)$(MSBuildProjectName).sourcelink.json + + <_SourceLinkFilePath>$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)', '$(MSBuildProjectName).sourcelink.json')) diff --git a/src/SourceLink.Git.IntegrationTests/AzureDevOpsServerTests.cs b/src/SourceLink.Git.IntegrationTests/AzureDevOpsServerTests.cs index b4dd58940f..4c88b85d77 100644 --- a/src/SourceLink.Git.IntegrationTests/AzureDevOpsServerTests.cs +++ b/src/SourceLink.Git.IntegrationTests/AzureDevOpsServerTests.cs @@ -54,7 +54,7 @@ public void FullValidation_Https() ProjectSourceRoot, $"https://tfs.{TestStrings.DomainName}.local:8080/tfs/DefaultCollection/project/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://tfs.{TestStrings.DomainName}.local:8080/tfs/DefaultCollection/project/_git/{repoName}", $"https://tfs.{TestStrings.DomainName}.local:8080/tfs/DefaultCollection/project/_git/{repoName}", }); @@ -114,7 +114,7 @@ public void FullValidation_Ssh() ProjectSourceRoot, $"https://tfs.{TestStrings.DomainName}.local/tfs/DefaultCollection/project/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://tfs.{TestStrings.DomainName}.local/tfs/DefaultCollection/project/_git/{repoName}", $"https://tfs.{TestStrings.DomainName}.local/tfs/DefaultCollection/project/_git/{repoName}", }); diff --git a/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs b/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs index 4755f68001..b4583d8c30 100644 --- a/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs +++ b/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs @@ -54,7 +54,7 @@ public void FullValidation_Https(string host) ProjectSourceRoot, $"https://test.{host}/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://test.{host}/test-org/_git/{repoName}", $"https://test.{host}/test-org/_git/{repoName}", }); @@ -113,7 +113,7 @@ public void FullValidation_Ssh(string host) ProjectSourceRoot, $"https://test.{host}/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://test.{host}/test-org/_git/{repoName}", $"https://test.{host}/test-org/_git/{repoName}", }); diff --git a/src/SourceLink.Git.IntegrationTests/BitbucketGitTests.cs b/src/SourceLink.Git.IntegrationTests/BitbucketGitTests.cs index 8132cb027e..e6700da121 100644 --- a/src/SourceLink.Git.IntegrationTests/BitbucketGitTests.cs +++ b/src/SourceLink.Git.IntegrationTests/BitbucketGitTests.cs @@ -50,7 +50,7 @@ public void FullValidation_CloudHttps() ProjectSourceRoot, $"https://api.bitbucket.org/2.0/repositories/test-org/{repoName}/src/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://bitbucket.org/test-org/{repoName}", $"https://bitbucket.org/test-org/{repoName}" }); @@ -111,7 +111,7 @@ public void FullValidation_EnterpriseNewHttps() ProjectSourceRoot, $"https://bitbucket.domain.com/projects/test-org/repos/{repoName}/raw/*?at={commitSha}", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://bitbucket.domain.com/scm/test-org/{repoName}", $"https://bitbucket.domain.com/scm/test-org/{repoName}" }); @@ -170,7 +170,7 @@ public void FullValidation_EnterpriseNewHttps_UrlWithPersonalToken() ProjectSourceRoot, $"https://bitbucket.domain.com/projects/test-org/repos/{repoName}/raw/*?at={commitSha}", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://bitbucket.domain.com/scm/test-org/{repoName}.git", $"https://bitbucket.domain.com/scm/test-org/{repoName}.git" }); @@ -229,7 +229,7 @@ public void FullValidation_EnterpriseNewHttpsWithDefaultFlags() NuGetPackageFolders, ProjectSourceRoot, $"https://bitbucket.domain.com/projects/test-org/repos/{repoName}/raw/*?at={commitSha}", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://bitbucket.domain.com/scm/test-org/{repoName}", $"https://bitbucket.domain.com/scm/test-org/{repoName}" }); @@ -289,7 +289,7 @@ public void FullValidation_EnterpriseOldHttps() ProjectSourceRoot, $"https://bitbucket.domain.com/projects/test-org/repos/{repoName}/browse/*?at={commitSha}&raw", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://bitbucket.domain.com/scm/test-org/{repoName}", $"https://bitbucket.domain.com/scm/test-org/{repoName}" }); @@ -349,7 +349,7 @@ public void FullValidation_CloudSsh() ProjectSourceRoot, $"https://api.{TestStrings.DomainName}.com/2.0/repositories/test-org/{repoName}/src/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{TestStrings.DomainName}.com/test-org/{repoName}", $"https://{TestStrings.DomainName}.com/test-org/{repoName}" }); @@ -409,7 +409,7 @@ public void FullValidation_EnterpriseOldSsh() ProjectSourceRoot, $"https://{TestStrings.DomainName}.com/projects/test-org/repos/{repoName}/browse/*?at={commitSha}&raw", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{TestStrings.DomainName}.com/test-org/{repoName}", $"https://{TestStrings.DomainName}.com/test-org/{repoName}" }); @@ -469,7 +469,7 @@ public void FullValidation_EnterpriseNewSsh() ProjectSourceRoot, $"https://{TestStrings.DomainName}.com/projects/test-org/repos/{repoName}/raw/*?at={commitSha}", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{TestStrings.DomainName}.com/test-org/{repoName}", $"https://{TestStrings.DomainName}.com/test-org/{repoName}" }); diff --git a/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs b/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs index 56cac30370..9ad9d8eadc 100644 --- a/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs +++ b/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs @@ -301,10 +301,10 @@ public void CustomTranslation() ProjectSourceRoot, $"https://raw.githubusercontent.com/test-org/{repoName}/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://github.com/test-org/{repoName}", $"https://github.com/test-org/{repoName}", - s_relativeSourceLinkJsonPath + SourceLinkFilePath }); AssertEx.AreEqual( @@ -361,7 +361,7 @@ public void Host_VisualStudio(string host) ProjectSourceRoot, $"https://test.{host}/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://test.{host}/test-org/_git/{repoName}", $"https://test.{host}/test-org/_git/{repoName}", }); @@ -405,7 +405,7 @@ public void Host_DevAzureCom(string host) ProjectSourceRoot, $"https://{host}/test/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{host}/test/test-org/_git/{repoName}", $"https://{host}/test/test-org/_git/{repoName}", }); diff --git a/src/SourceLink.Git.IntegrationTests/GitHubTests.cs b/src/SourceLink.Git.IntegrationTests/GitHubTests.cs index 3d58b41399..0158ffb8b0 100644 --- a/src/SourceLink.Git.IntegrationTests/GitHubTests.cs +++ b/src/SourceLink.Git.IntegrationTests/GitHubTests.cs @@ -68,7 +68,7 @@ public void MutlipleProjects() SourceRoot, $"https://raw.githubusercontent.com/test-org/test-repo2/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"http://github.com/test-org/test-repo2", }, // the second project should reuse the repository info cached by the first project: @@ -216,7 +216,7 @@ public void FullValidation_Https() ProjectSourceRoot, $"https://raw.githubusercontent.com/test-org/{repoName}/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"http://github.com/test-org/{repoName}", $"http://github.com/test-org/{repoName}" }); @@ -274,7 +274,7 @@ public void FullValidation_Ssh() ProjectSourceRoot, $"https://raw.githubusercontent.com/test-org/{repoName}/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://github.com/test-org/{repoName}", $"https://github.com/test-org/{repoName}" }); diff --git a/src/SourceLink.Git.IntegrationTests/GitLabTests.cs b/src/SourceLink.Git.IntegrationTests/GitLabTests.cs index f36e46dea3..07d8aa5399 100644 --- a/src/SourceLink.Git.IntegrationTests/GitLabTests.cs +++ b/src/SourceLink.Git.IntegrationTests/GitLabTests.cs @@ -54,7 +54,7 @@ public void FullValidation_Https() ProjectSourceRoot, $"https://{TestStrings.DomainName}.com/test-org/{repoName}/-/raw/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{TestStrings.DomainName}.com/test-org/{repoName}", $"https://{TestStrings.DomainName}.com/test-org/{repoName}" }); @@ -114,7 +114,7 @@ public void FullValidation_Ssh() ProjectSourceRoot, $"https://{TestStrings.DomainName}.com/test-org/{repoName}/-/raw/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{TestStrings.DomainName}.com/test-org/{repoName}", $"https://{TestStrings.DomainName}.com/test-org/{repoName}" }); diff --git a/src/SourceLink.Git.IntegrationTests/GitWebTests.cs b/src/SourceLink.Git.IntegrationTests/GitWebTests.cs index 2a5a8aa7ff..653bac3b29 100644 --- a/src/SourceLink.Git.IntegrationTests/GitWebTests.cs +++ b/src/SourceLink.Git.IntegrationTests/GitWebTests.cs @@ -57,7 +57,7 @@ public void FullValidation_Ssh() ProjectSourceRoot, $"https://{TestStrings.DomainName}.com/gitweb/?p={repoName};a=blob_plain;hb={commitSha};f=*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"ssh://git@{TestStrings.DomainName}.com/{repoNameFullyEscaped}", $"ssh://git@{TestStrings.DomainName}.com/{repoNameFullyEscaped}" }); diff --git a/src/SourceLink.Git.IntegrationTests/GiteaTests.cs b/src/SourceLink.Git.IntegrationTests/GiteaTests.cs index 78ddc8fe65..894b4eabaf 100644 --- a/src/SourceLink.Git.IntegrationTests/GiteaTests.cs +++ b/src/SourceLink.Git.IntegrationTests/GiteaTests.cs @@ -54,7 +54,7 @@ public void FullValidation_Https() ProjectSourceRoot, $"https://{TestStrings.DomainName}.com/test-org/{repoName}/raw/commit/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{TestStrings.DomainName}.com/test-org/{repoName}", $"https://{TestStrings.DomainName}.com/test-org/{repoName}" }); @@ -114,7 +114,7 @@ public void FullValidation_Ssh() ProjectSourceRoot, $"https://{TestStrings.DomainName}.com/test-org/{repoName}/raw/commit/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{TestStrings.DomainName}.com/test-org/{repoName}", $"https://{TestStrings.DomainName}.com/test-org/{repoName}" }); diff --git a/src/SourceLink.Git.IntegrationTests/GiteeTests.cs b/src/SourceLink.Git.IntegrationTests/GiteeTests.cs index aa183d79b9..051bf8a91e 100644 --- a/src/SourceLink.Git.IntegrationTests/GiteeTests.cs +++ b/src/SourceLink.Git.IntegrationTests/GiteeTests.cs @@ -54,7 +54,7 @@ public void FullValidation_Https() ProjectSourceRoot, $"https://{TestStrings.DomainName}.com/test-org/{repoName}/raw/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{TestStrings.DomainName}.com/test-org/{repoName}", $"https://{TestStrings.DomainName}.com/test-org/{repoName}" }); @@ -114,7 +114,7 @@ public void FullValidation_Ssh() ProjectSourceRoot, $"https://{TestStrings.DomainName}.com/test-org/{repoName}/raw/{commitSha}/*", "refs/heads/main", - s_relativeSourceLinkJsonPath, + SourceLinkFilePath, $"https://{TestStrings.DomainName}.com/test-org/{repoName}", $"https://{TestStrings.DomainName}.com/test-org/{repoName}" }); diff --git a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs index 5af1618d39..7c15bce4db 100644 --- a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs +++ b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs @@ -75,6 +75,10 @@ private static string GetLocalNuGetConfigContent(string packagesDir) => protected readonly string DotNetPath; protected readonly Dictionary EnvironmentVariables; + // Absolute path of the generated source link file. The source link targets pass an absolute OutputFile + // to the GenerateSourceLinkFile task, so $(SourceLink) (and the corresponding FileWrites item) is absolute. + protected readonly string SourceLinkFilePath; + protected static readonly string s_relativeSourceLinkJsonPath = Path.Combine("obj", "Debug", "netstandard2.0", "test.sourcelink.json"); protected static readonly string s_relativeOutputFilePath = Path.Combine("obj", "Debug", "netstandard2.0", "test.dll"); protected static readonly string s_relativePackagePath = Path.Combine("bin", "Debug", "test.1.0.0.nupkg"); @@ -213,6 +217,8 @@ public DotNetSdkTestBase(params string[] packages) Project = ProjectDir.CreateFile(ProjectFileName).WriteAllText(s_projectSource); ProjectDir.CreateFile("TestClass.cs").WriteAllText(s_classSource); + + SourceLinkFilePath = Path.Combine(ProjectDir.Path, s_relativeSourceLinkJsonPath); } public static string EnsureTrailingDirectorySeparator(string path)