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)