| File: ProduceContentAssets.cs | Web Access |
| Project: src\sdk\src\Tasks\Microsoft.NET.Build.Tasks\Microsoft.NET.Build.Tasks.csproj (Microsoft.NET.Build.Tasks) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable disable using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace Microsoft.NET.Build.Tasks { /// <summary> /// Read raised lock file items for content assets and process them to handle /// preprocessing tokens, identify items that should be copied to output, and /// other filtering on content assets, including whether they match the active /// project language. /// </summary> public sealed class ProduceContentAssets : TaskBase { private readonly List<ITaskItem> _contentItems = new(); private readonly List<ITaskItem> _fileWrites = new(); private readonly List<ITaskItem> _copyLocalItems = new(); private IContentAssetPreprocessor _assetPreprocessor; #region Output Items /// <summary> /// Content items that are marked copy to output with resolved path /// </summary> [Output] public ITaskItem[] CopyLocalItems => _copyLocalItems.ToArray(); /// <summary> /// All content items produced with content item metadata. /// </summary> [Output] public ITaskItem[] ProcessedContentItems => _contentItems.ToArray(); /// <summary> /// Files written to during the generation process. /// </summary> [Output] public ITaskItem[] FileWrites => _fileWrites.ToArray(); #endregion #region Inputs /// <summary> /// Resolved paths to content file assets with metadata such as BuildAction, PPOutputPath etc. /// </summary> [Required] public ITaskItem[] ContentFileDependencies { get; set; } /// <summary> /// Items specifying the tokens that can be substituted into preprocessed /// content files. The ItemSpec of each item is the name of the token, /// without the surrounding $$, and the Value metadata should specify the /// replacement value. /// </summary> public ITaskItem[] ContentPreprocessorValues { get; set; } /// <summary> /// The base output directory where the temporary, preprocessed files should be written to. /// </summary> public string ContentPreprocessorOutputDirectory { get; set; } /// <summary> /// Optional the Project Language (E.g. C#, VB) /// </summary> public string ProjectLanguage { get; set; } /// <summary> /// Optionally filter the operation of this task to just preprocessor files /// </summary> public bool ProduceOnlyPreprocessorFiles { get; set; } #endregion public ProduceContentAssets() { } #region Test Support internal ProduceContentAssets(IContentAssetPreprocessor assetPreprocessor) : this() { _assetPreprocessor = assetPreprocessor; } #endregion /// <summary> /// Resource for reading, processing and writing content assets /// </summary> internal IContentAssetPreprocessor AssetPreprocessor { get { if (_assetPreprocessor == null) { _assetPreprocessor = new NugetContentAssetPreprocessor(); } return _assetPreprocessor; } } protected override void ExecuteCore() { var preprocessorValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); // If a preprocessor directory isn't set, then we won't have a place to generate. if (!string.IsNullOrEmpty(ContentPreprocessorOutputDirectory)) { // Assemble the preprocessor values up-front var duplicatedPreprocessorKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (var preprocessorValueItem in ContentPreprocessorValues ?? Enumerable.Empty<ITaskItem>()) { if (preprocessorValues.ContainsKey(preprocessorValueItem.ItemSpec)) { duplicatedPreprocessorKeys.Add(preprocessorValueItem.ItemSpec); } preprocessorValues[preprocessorValueItem.ItemSpec] = preprocessorValueItem.GetMetadata("Value"); } foreach (var duplicatedPreprocessorKey in duplicatedPreprocessorKeys) { Log.LogWarning(Strings.DuplicatePreprocessorToken, duplicatedPreprocessorKey, preprocessorValues[duplicatedPreprocessorKey]); } AssetPreprocessor.ConfigurePreprocessor(ContentPreprocessorOutputDirectory, preprocessorValues); } var contentFileDeps = ContentFileDependencies ?? Enumerable.Empty<ITaskItem>(); var contentFileGroups = contentFileDeps .Where(f => !ProduceOnlyPreprocessorFiles || IsPreprocessorFile(f)) .GroupBy(t => t.GetMetadata(MetadataKeys.NuGetPackageId)); foreach (var grouping in contentFileGroups) { // Is there an asset with our exact language? If so, we use that. Otherwise we'll simply collect "any" assets. string codeLanguageToSelect; if (string.IsNullOrEmpty(ProjectLanguage)) { codeLanguageToSelect = "any"; } else { string projectLanguage = NuGetUtils.GetLockFileLanguageName(ProjectLanguage); if (grouping.Any(t => t.GetMetadata(MetadataKeys.CodeLanguage) == projectLanguage)) { codeLanguageToSelect = projectLanguage; } else { codeLanguageToSelect = "any"; } } foreach (var contentFile in grouping) { // Ignore magic _._ placeholder files. We couldn't ignore them during the project language // selection, since you could imagine somebody might have a package that puts assets under // "any" but then uses _._ to opt some languages out of it if (NuGetUtils.IsPlaceholderFile(contentFile.ItemSpec)) { continue; } if (contentFile.GetMetadata(MetadataKeys.CodeLanguage) == codeLanguageToSelect) { ProduceContentAsset(contentFile); } } } } private bool IsPreprocessorFile(ITaskItem contentFile) => !string.IsNullOrEmpty(contentFile.GetMetadata(MetadataKeys.PPOutputPath)); private void ProduceContentAsset(ITaskItem contentFile) { string resolvedPath = contentFile.ItemSpec; string pathToFinalAsset = resolvedPath; string ppOutputPath = contentFile.GetMetadata(MetadataKeys.PPOutputPath); string packageName = contentFile.GetMetadata(MetadataKeys.NuGetPackageId); string packageVersion = contentFile.GetMetadata(MetadataKeys.NuGetPackageVersion); if (!string.IsNullOrEmpty(ppOutputPath)) { if (string.IsNullOrEmpty(ContentPreprocessorOutputDirectory)) { throw new BuildErrorException(Strings.ContentPreproccessorParameterRequired, nameof(ProduceContentAssets), nameof(ContentPreprocessorOutputDirectory)); } // We need the preprocessed output, so let's run the preprocessor here string relativeOutputPath = Path.Combine(packageName, packageVersion, ppOutputPath); if (AssetPreprocessor.Process(resolvedPath, relativeOutputPath, out pathToFinalAsset)) { _fileWrites.Add(new TaskItem(pathToFinalAsset)); } } if (contentFile.GetBooleanMetadata(MetadataKeys.CopyToOutput) == true) { string outputPath = contentFile.GetMetadata(MetadataKeys.OutputPath); outputPath = string.IsNullOrEmpty(outputPath) ? ppOutputPath : outputPath; if (!string.IsNullOrEmpty(outputPath)) { var item = new TaskItem(pathToFinalAsset); item.SetMetadata("TargetPath", outputPath); item.SetMetadata(MetadataKeys.NuGetPackageId, packageName); item.SetMetadata(MetadataKeys.NuGetPackageVersion, packageVersion); _copyLocalItems.Add(item); } else { Log.LogWarning(Strings.ContentItemDoesNotProvideOutputPath, pathToFinalAsset, MetadataKeys.CopyToOutput, MetadataKeys.OutputPath, MetadataKeys.PPOutputPath); } } // TODO if build action is none do we even need to write the processed file above? string buildAction = contentFile.GetMetadata(MetadataKeys.BuildAction); if (!string.Equals(buildAction, "none", StringComparison.OrdinalIgnoreCase)) { var item = new TaskItem(pathToFinalAsset); item.SetMetadata(MetadataKeys.NuGetPackageId, packageName); item.SetMetadata(MetadataKeys.NuGetPackageVersion, packageVersion); // We'll put additional metadata on the item so we can convert it back to the real item group in our targets item.SetMetadata("ProcessedItemType", buildAction); // TODO is this needed for .NETCore? // If this is XAML, the build targets expect Link metadata to construct the relative path if (string.Equals(buildAction, "Page", StringComparison.OrdinalIgnoreCase)) { item.SetMetadata("Link", Path.Combine("NuGet", packageName, packageVersion, Path.GetFileName(resolvedPath))); } _contentItems.Add(item); } } } }