File: src\GenerateSourcePackageSourceLinkTargetsFile.cs
Web Access
Project: src\src\Microsoft.DotNet.Arcade.Sdk\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
{
    public sealed class GenerateSourcePackageSourceLinkTargetsFile : Microsoft.Build.Utilities.Task
    {
        [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()
        {
            ExecuteImpl();
            return !Log.HasLoggedErrors;
        }
 
        private void ExecuteImpl()
        {
            Directory.CreateDirectory(Path.GetDirectoryName(OutputPath));
            File.WriteAllText(OutputPath, GetOutputFileContent(), Encoding.UTF8);
        }
 
        // 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>
";
        }
    }
}