File: src\GenerateSourcePackageSourceLinkTargetsFile.cs
Web Access
Project: Microsoft.DotNet.Arcade.Sdk.csproj (Microsoft.DotNet.Arcade.Sdk)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.DotNet.Arcade.Sdk;

[MSBuildMultiThreadableTask]
public sealed class GenerateSourcePackageSourceLinkTargetsFile : Task, IMultiThreadableTask
{
    /// <summary>Injected by MSBuild so paths resolve against the project directory in multithreaded builds.</summary>
    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

    [Required]
    public string ProjectDirectory { get; set; }

    [Required]
    public string PackageId { get; set; }

    [Required]
    public ITaskItem[] SourceRoots { get; set; }

    [Required]
    public string OutputPath { get; set; }

    public override bool Execute()
    {
        AbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(OutputPath);
        Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
        File.WriteAllText(outputPath, GetOutputFileContent(), Encoding.UTF8);

        return !Log.HasLoggedErrors;
    }

    // for testing
    internal string GetOutputFileContent()
    {
        var projectDir = EndWithSeparator(ProjectDirectory);

        string innerMostRootItemSpec = null;
        string innerMostRootSourceLinkUrl = null;
        foreach (var sourceRoot in SourceRoots)
        {
            var itemSpec = sourceRoot.ItemSpec;
            if (itemSpec.Length > projectDir.Length && itemSpec.StartsWith(projectDir, StringComparison.Ordinal))
            {
                Log.LogError($"Directory '{ProjectDirectory}' contains source roots (e.g. git submodules), which is not supported.");
                return null;
            }

            if (!projectDir.StartsWith(itemSpec, StringComparison.Ordinal) ||
                innerMostRootItemSpec != null && itemSpec.Length <= innerMostRootItemSpec.Length)
            {
                continue;
            }

            var url = sourceRoot.GetMetadata("SourceLinkUrl");
            if (string.IsNullOrEmpty(url))
            {
                continue;
            }

            innerMostRootItemSpec = itemSpec;
            innerMostRootSourceLinkUrl = url;
        }

        if (innerMostRootSourceLinkUrl == null)
        {
            Log.LogError($"No SourceRoot with SourceLinkUrl contains directory '{ProjectDirectory}'.");
            return null;
        }

        if (innerMostRootSourceLinkUrl.Count(c => c == '*') != 1)
        {
            Log.LogError($"SourceLinkUrl must contain exactly one '*': '{innerMostRootSourceLinkUrl}'");
            return null;
        }

        if (!EndsWithSeparator(innerMostRootItemSpec))
        {
            Log.LogError($"SourceRoot must end with a directory separator: '{innerMostRootItemSpec}'");
            return null;
        }

        var relativePathToSourceRoot = projectDir.Substring(innerMostRootItemSpec.Length);
        var contentFilesSourceLinkUrl = innerMostRootSourceLinkUrl.Replace("*", Uri.EscapeUriString(relativePathToSourceRoot.Replace('\\', '/')) + "*");
        return GetTargetsFileContent(PackageId, contentFilesSourceLinkUrl);
    }

    private static bool EndsWithSeparator(string path)
    {
        char last = path[path.Length - 1];
        return last == Path.DirectorySeparatorChar || last == Path.AltDirectorySeparatorChar;
    }

    private static string EndWithSeparator(string path)
        => EndsWithSeparator(path) ? path : path + Path.DirectorySeparatorChar;

    private static string GetTargetsFileContent(string packageId, string sourceLinkUrl)
    {
        string hash;
        using (var hashAlg = SHA256.Create())
        {
            hash = BitConverter.ToString(hashAlg.ComputeHash(Encoding.UTF8.GetBytes(packageId)), 0, 20).Replace("-", "");
        }

        return $@"<?xml version=""1.0"" encoding=""utf-8""?>
<Project xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
  <Target Name=""_AddSourcePackageSourceRoot_{hash}"" BeforeTargets=""InitializeSourceControlInformation"">
    <ItemGroup>
      <_PackageCompileItems Remove=""@(_PackageCompileItems)""/>
      <_PackageCompileItems Include=""%(Compile.RootDir)%(Compile.Directory)"" Condition=""'%(Compile.NuGetPackageId)' == '{packageId}'"" />
    </ItemGroup>

    <PropertyGroup>
      <_PackageCompileItem>@(_PackageCompileItems)</_PackageCompileItem>
      <_PackageCompileItem Condition=""'$(_PackageCompileItem)' != ''"">$(_PackageCompileItem.Split(';')[0])</_PackageCompileItem>
    </PropertyGroup>

    <ItemGroup>
      <SourceRoot Include=""$([System.Text.RegularExpressions.Regex]::Match($(_PackageCompileItem), '^$([System.Text.RegularExpressions.Regex]::Escape($([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../contentFiles/'))))([^\\/]+[\\/][^\\/]+[\\/])'))"" 
                  SourceLinkUrl=""{sourceLinkUrl}""/>
    </ItemGroup>
  </Target>
</Project>
";
    }
}