File: src\BuildModelFactory.cs
Web Access
Project: src\src\Microsoft.DotNet.Build.Tasks.Feed\Microsoft.DotNet.Build.Tasks.Feed.csproj (Microsoft.DotNet.Build.Tasks.Feed)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Arcade.Common;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.DotNet.VersionTools.BuildManifest.Model;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
 
namespace Microsoft.DotNet.Build.Tasks.Feed
{
    public interface IBuildModelFactory
    {
        void CreateBuildManifest(
            IEnumerable<BlobArtifactModel> blobArtifacts,
            IEnumerable<PackageArtifactModel> packageArtifacts,
            string assetManifestPath,
            string manifestRepoName,
            string manifestBuildId,
            string manifestBranch,
            string manifestCommit,
            string[] manifestBuildData,
            bool isStableBuild,
            PublishingInfraVersion publishingVersion,
            bool isReleaseOnlyPackageVersion,
            SigningInformationModel signingInformationModel = null);
 
        BuildModel CreateModelFromItems(
            ITaskItem[] artifacts,
            ITaskItem[] itemsToSign,
            ITaskItem[] strongNameSignInfo,
            ITaskItem[] fileSignInfo,
            ITaskItem[] fileExtensionSignInfo,
            ITaskItem[] certificatesSignInfo,
            string buildId,
            string[] manifestBuildData,
            string repoUri,
            string repoBranch,
            string repoCommit,
            string repoOrigin,
            bool isStableBuild,
            PublishingInfraVersion publishingVersion,
            bool isReleaseOnlyPackageVersion);
 
        BuildModel ManifestFileToModel(string assetManifestPath);
    }
 
    public class BuildModelFactory : IBuildModelFactory
    {
        private readonly ISigningInformationModelFactory _signingInformationModelFactory;
        private readonly IBlobArtifactModelFactory _blobArtifactModelFactory;
        private readonly IPackageArtifactModelFactory _packageArtifactModelFactory;
        private readonly IFileSystem _fileSystem;
        private readonly TaskLoggingHelper _log;
 
        public BuildModelFactory(
            ISigningInformationModelFactory signingInformationModelFactory,
            IBlobArtifactModelFactory blobArtifactModelFactory,
            IPackageArtifactModelFactory packageArtifactModelFactory,
            IFileSystem fileSystem,
            TaskLoggingHelper logger)
        {
            _signingInformationModelFactory = signingInformationModelFactory;
            _blobArtifactModelFactory = blobArtifactModelFactory;
            _packageArtifactModelFactory = packageArtifactModelFactory;
            _fileSystem = fileSystem;
            _log = logger;
        }
 
        private const string AssetsVirtualDir = "assets/";
 
        private static readonly string AzureDevOpsHostPattern = @"dev\.azure\.com\";
 
        private readonly Regex LegacyRepositoryUriPattern = new Regex(
            @"^https://(?<account>[a-zA-Z0-9]+)\.visualstudio\.com/");
 
        /// <summary>
        /// Create a build manifest for packages, blobs, and associated signing information
        /// </summary>
        /// <param name="log">MSBuild log helper</param>
        /// <param name="blobArtifacts">Collection of blobs</param>
        /// <param name="packageArtifacts">Collection of packages</param>
        /// <param name="assetManifestPath">Asset manifest file that should be written</param>
        /// <param name="manifestRepoName">Repository name</param>
        /// <param name="manifestBuildId">Azure devops build id</param>
        /// <param name="manifestBranch">Name of the branch that was built</param>
        /// <param name="manifestCommit">Commit that was built</param>
        /// <param name="manifestBuildData">Additional build data properties</param>
        /// <param name="isStableBuild">True if the build is stable, false otherwise.</param>
        /// <param name="publishingVersion">Publishing version in use.</param>
        /// <param name="isReleaseOnlyPackageVersion">True if this repo uses release-only package versions</param>
        /// <param name="signingInformationModel">Signing information.</param>
        public void CreateBuildManifest(
            IEnumerable<BlobArtifactModel> blobArtifacts,
            IEnumerable<PackageArtifactModel> packageArtifacts,
            string assetManifestPath,
            string manifestRepoName,
            string manifestBuildId,
            string manifestBranch,
            string manifestCommit,
            string[] manifestBuildData,
            bool isStableBuild,
            PublishingInfraVersion publishingVersion,
            bool isReleaseOnlyPackageVersion,
            SigningInformationModel signingInformationModel = null)
        {
            BuildModel model = CreateModel(
                blobArtifacts,
                packageArtifacts,
                manifestBuildId,
                manifestBuildData,
                manifestRepoName,
                manifestBranch,
                manifestCommit,
                isStableBuild,
                publishingVersion,
                isReleaseOnlyPackageVersion,
                signingInformationModel: signingInformationModel);
 
            _log.LogMessage(MessageImportance.High, $"Writing build manifest file '{assetManifestPath}'...");
            _fileSystem.WriteToFile(assetManifestPath, model.ToXml().ToString(SaveOptions.DisableFormatting));
        }
 
        public BuildModel CreateModelFromItems(
            ITaskItem[] artifacts,
            ITaskItem[] itemsToSign,
            ITaskItem[] strongNameSignInfo,
            ITaskItem[] fileSignInfo,
            ITaskItem[] fileExtensionSignInfo,
            ITaskItem[] certificatesSignInfo,
            string buildId,
            string[] manifestBuildData,
            string repoUri,
            string repoBranch,
            string repoCommit,
            string repoOrigin,
            bool isStableBuild,
            PublishingInfraVersion publishingVersion,
            bool isReleaseOnlyPackageVersion)
        {
            if (artifacts == null)
            {
                throw new ArgumentNullException(nameof(artifacts));
            }
 
            var blobArtifacts = new List<BlobArtifactModel>();
            var packageArtifacts = new List<PackageArtifactModel>();
 
            foreach (var artifact in artifacts)
            {
                if (string.Equals(artifact.GetMetadata("ExcludeFromManifest"), "true", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }
 
                var isSymbolsPackage = GeneralUtils.IsSymbolPackage(artifact.ItemSpec);
 
                if (artifact.ItemSpec.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase) && !isSymbolsPackage)
                {
                    packageArtifacts.Add(_packageArtifactModelFactory.CreatePackageArtifactModel(artifact, repoOrigin));
                }
                else
                {
                    if (isSymbolsPackage)
                    {
                        string fileName = Path.GetFileName(artifact.ItemSpec);
                        artifact.SetMetadata("RelativeBlobPath", $"{AssetsVirtualDir}symbols/{fileName}");
                    }
 
                    blobArtifacts.Add(_blobArtifactModelFactory.CreateBlobArtifactModel(artifact, repoOrigin));
                }
            }
 
            var signingInfoModel = _signingInformationModelFactory.CreateSigningInformationModelFromItems(
                itemsToSign, strongNameSignInfo, fileSignInfo, fileExtensionSignInfo,
                certificatesSignInfo, blobArtifacts, packageArtifacts);
 
            var buildModel = CreateModel(
                blobArtifacts,
                packageArtifacts,
                buildId,
                manifestBuildData,
                repoUri,
                repoBranch,
                repoCommit,
                isStableBuild,
                publishingVersion,
                isReleaseOnlyPackageVersion,
                signingInformationModel: signingInfoModel);
            return buildModel;
        }
 
        private BuildModel CreateModel(
            IEnumerable<BlobArtifactModel> blobArtifacts,
            IEnumerable<PackageArtifactModel> packageArtifacts,
            string manifestBuildId,
            string[] manifestBuildData,
            string manifestRepoName,
            string manifestBranch,
            string manifestCommit,
            bool isStableBuild,
            PublishingInfraVersion publishingVersion,
            bool isReleaseOnlyPackageVersion,
            SigningInformationModel signingInformationModel = null)
        {
            var attributes = MSBuildListSplitter.GetNamedProperties(manifestBuildData);
            if (!ManifestBuildDataHasLocationInformation(attributes))
            {
                _log.LogError("Missing 'location' property from ManifestBuildData");
            }
 
            NormalizeUrisInBuildData(attributes);
 
            BuildModel buildModel = new BuildModel(
                    new BuildIdentity
                    {
                        Attributes = attributes,
                        Name = manifestRepoName,
                        BuildId = manifestBuildId,
                        Branch = manifestBranch,
                        Commit = manifestCommit,
                        IsStable = isStableBuild,
                        PublishingVersion = publishingVersion,
                        IsReleaseOnlyPackageVersion = isReleaseOnlyPackageVersion
                    });
 
            buildModel.Artifacts.Blobs.AddRange(blobArtifacts);
            buildModel.Artifacts.Packages.AddRange(packageArtifacts);
            buildModel.SigningInformation = signingInformationModel;
            return buildModel;
        }
 
        public BuildModel ManifestFileToModel(string assetManifestPath)
        {
            try
            {
                return BuildModel.Parse(XElement.Load(assetManifestPath));
            }
            catch (Exception e)
            {
                _log.LogError($"Could not parse asset manifest file: {assetManifestPath}");
                _log.LogErrorFromException(e);
                return null;
            }
        }
 
        private bool ManifestBuildDataHasLocationInformation(IDictionary<string, string> attributes)
        {
            return attributes.ContainsKey("Location") || attributes.ContainsKey("InitialAssetsLocation");
        }
 
        private void NormalizeUrisInBuildData(IDictionary<string, string> attributes)
        {
            foreach(var attribute in attributes.ToList())
            {
                attributes[attribute.Key] = NormalizeAzureDevOpsUrl(attribute.Value);
            }
        }
 
        /// <summary>
        // If repoUri includes the user in the account we remove it from URIs like
        // https://dnceng@dev.azure.com/dnceng/internal/_git/repo
        // If the URL host is of the form "dnceng.visualstudio.com" like
        // https://dnceng.visualstudio.com/internal/_git/repo we replace it to "dev.azure.com/dnceng"
        // for consistency
        /// </summary>
        /// <param name="repoUri">The original url</param>
        /// <returns>Transformed url</returns>
        private string NormalizeAzureDevOpsUrl(string repoUri)
        {
            if (Uri.TryCreate(repoUri, UriKind.Absolute, out Uri parsedUri))
            {
                if (!string.IsNullOrEmpty(parsedUri.UserInfo))
                {
                    repoUri = repoUri.Replace($"{parsedUri.UserInfo}@", string.Empty);
                }
 
                Match m = LegacyRepositoryUriPattern.Match(repoUri);
 
                if (m.Success)
                {
                    string replacementUri = $"{Regex.Unescape(AzureDevOpsHostPattern)}/{m.Groups["account"].Value}";
                    repoUri = repoUri.Replace(parsedUri.Host, replacementUri);
                }
            }
 
            return repoUri;
        }
    }
}