| File: GenerateSourceLinkFile.cs | Web Access |
| Project: src\sourcelink\src\SourceLink.Common\Microsoft.SourceLink.Common.csproj (Microsoft.SourceLink.Common) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the License.txt file in the project root for more information. using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Tasks.SourceControl; using Microsoft.Build.Utilities; namespace Microsoft.SourceLink.Common { [MSBuildMultiThreadableTask] public sealed class GenerateSourceLinkFile : Task { [Required, NotNull] public ITaskItem[]? SourceRoots { get; set; } [Required, NotNull] public string? OutputFile { get; set; } /// <summary> /// Set to <see cref="OutputFile"/> if the output Source Link file should be passed to the compiler. /// </summary> [Output] public string? SourceLink { get; set; } public bool NoWarnOnMissingSourceControlInformation { get; set; } public override bool Execute() { WriteSourceLinkFile(GenerateSourceLinkContent()); return !Log.HasLoggedErrors; } internal string? GenerateSourceLinkContent() { static string jsonEscape(string str) => str.Replace(@"\", @"\\").Replace("\"", "\\\""); var result = new StringBuilder(); result.Append("{\"documents\":{"); var success = true; var isEmpty = true; foreach (var root in SourceRoots) { var mappedPath = root.GetMetadata(Names.SourceRoot.MappedPath); var isMapped = !string.IsNullOrEmpty(mappedPath); var localPath = isMapped ? mappedPath : root.ItemSpec; if (!localPath.EndsWithSeparator()) { Log.LogError(Resources.MustEndWithDirectorySeparator, isMapped ? Names.SourceRoot.MappedPathFullName : Names.SourceRoot.Name, localPath); success = false; continue; } if (localPath.Contains('*')) { Log.LogError(Resources.MustNotContainWildcard, isMapped ? Names.SourceRoot.MappedPathFullName : Names.SourceRoot.Name, localPath); success = false; continue; } var url = root.GetMetadata(Names.SourceRoot.SourceLinkUrl); if (string.IsNullOrEmpty(url)) { // Do not report any diagnostic. If the source root comes from source control a warning has already been reported. // SourceRoots can be specified by the project to make other features like deterministic paths, and they don't need source link URL. continue; } if (url.Count(c => c == '*') != 1) { Log.LogError(Resources.MustContainSingleWildcard, Names.SourceRoot.SourceLinkUrlFullName, url); success = false; continue; } if (isEmpty) { isEmpty = false; } else { result.Append(','); } result.Append('"'); result.Append(jsonEscape(localPath)); result.Append('*'); result.Append('"'); result.Append(':'); result.Append('"'); result.Append(jsonEscape(url)); result.Append('"'); } result.Append("}}"); return success && !isEmpty ? result.ToString() : null; } private void WriteSourceLinkFile(string? content) { if (content == null && !NoWarnOnMissingSourceControlInformation) { Log.LogWarning(Resources.SourceControlInformationIsNotAvailableGeneratedSourceLinkEmpty); } 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) { Log.LogMessage(Resources.SourceLinkEmptyDeletingExistingFile, OutputFile); File.Delete(OutputFile); return; } var originalContent = File.ReadAllText(OutputFile); if (originalContent == content) { // Don't rewrite the file if the contents is the same, just pass it to the compiler. Log.LogMessage(Resources.SourceLinkFileUpToDate, OutputFile); SourceLink = OutputFile; return; } } else if (content == null) { // File doesn't exist and the output is empty: // Do not write the file and don't pass it to the compiler. Log.LogMessage(Resources.SourceLinkEmptyNoExistingFile, OutputFile); return; } Log.LogMessage(Resources.SourceLinkFileUpdated, OutputFile); File.WriteAllText(OutputFile, content); SourceLink = OutputFile; } catch (Exception e) { Log.LogError(Resources.ErrorWritingToSourceLinkFile, OutputFile, e.Message); } } } }