diff --git a/src/mono/msbuild/common/CommonMobileBuild.props b/src/mono/msbuild/common/CommonMobileBuild.props
index 0124b52c2ef866..7224b772cb18ae 100644
--- a/src/mono/msbuild/common/CommonMobileBuild.props
+++ b/src/mono/msbuild/common/CommonMobileBuild.props
@@ -15,12 +15,5 @@
<_MonoRuntimeComponentManifestJsonFilePath Condition="'$(_MonoRuntimeComponentManifestJsonFilePath)' == '' and '$(_RuntimeComponentManifestDir)' != ''">$(_RuntimeComponentManifestDir)RuntimeComponentManifest.json
-
-
- <_MonoRuntimeComponentSharedLibExt ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
- <_MonoRuntimeComponentStaticLibExt ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
- <_MonoRuntimeComponentLinking ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
- <_MonoRuntimeAvailableComponents ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
-
-
+
\ No newline at end of file
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/README.md b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/README.md
index 0a0266015244db..326266a514a00c 100644
--- a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/README.md
+++ b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/README.md
@@ -37,6 +37,10 @@ To use the task in a project, reference the NuGet package, with the appropriate
```
+## MonoRuntimeComponentManifestReadTask
+
+The SDK registers `MonoRuntimeComponentManifestReadTask` from `MonoTargetsTasks.dll`. The task reads `RuntimeComponentManifest.json` and supplies the item groups consumed by `RuntimeComponentManifest.targets`.
+
## ILStrip
This is a task that removes the IL methods bodies from assemblies. Useful in conjunction with AOT compilation.
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/MonoTargetsTasks.props b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/MonoTargetsTasks.props
index 904ed6e2fdafc5..475a0c07d6950e 100644
--- a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/MonoTargetsTasks.props
+++ b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/MonoTargetsTasks.props
@@ -9,14 +9,5 @@
-
-
-
-
- <_MonoRuntimeComponentSharedLibExt ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
- <_MonoRuntimeComponentStaticLibExt ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
- <_MonoRuntimeComponentLinking ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
- <_MonoRuntimeAvailableComponents ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
-
-
+
diff --git a/src/mono/wasm/Wasm.Build.Tests/JsonManifestReaderTests.cs b/src/mono/wasm/Wasm.Build.Tests/JsonManifestReaderTests.cs
new file mode 100644
index 00000000000000..77b7a5c11d44f6
--- /dev/null
+++ b/src/mono/wasm/Wasm.Build.Tests/JsonManifestReaderTests.cs
@@ -0,0 +1,272 @@
+// 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.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+
+using Microsoft.Build.Framework;
+using Xunit;
+
+#nullable enable
+
+namespace Wasm.Build.Tests;
+
+[TestCategory("no-workload")]
+public sealed class JsonManifestReaderTests
+{
+ private const string JsonWithAllOutputs = """
+ {
+ "properties": {
+ "UnusedProperty": "still validated"
+ },
+ "items": {
+ "_MonoRuntimeComponentSharedLibExt": [
+ { "identity": ".dll", "RuntimeIdentifier": "win-x64" },
+ ],
+ "_MonoRuntimeComponentStaticLibExt": [
+ { "identity": ".lib", "RuntimeIdentifier": "win-x64" },
+ ],
+ "_MonoRuntimeComponentLinking": [
+ { "identity": "static", "RuntimeIdentifier": "win-x64" },
+ ],
+ "_MonoRuntimeAvailableComponents": [
+ { "identity": "diagnostics_tracing", "RuntimeIdentifier": "win-x64" },
+ ],
+ "EmccProperties": [
+ { "identity": "RuntimeEmccVersion", "value": "4.0.1" },
+ ],
+ "WasmOptConfigurationFlags": [
+ "--enable-simd",
+ { "identity": "--enable-threads", "source": "runtime-pack" },
+ ],
+ "EmccDefaultExportedRuntimeMethods": [],
+ "PropertiesThatTriggerRelinking": [
+ { "identity": "InvariantGlobalization", "defaultValueInRuntimePack": "false" },
+ ],
+ },
+ }
+ """;
+
+ [Fact]
+ public void ReadWasmPropsPreservesItemsMetadataAndMissingGroups()
+ {
+ using var directory = new TempDirectory();
+ string jsonPath = directory.WriteFile("input.json", JsonWithAllOutputs);
+ var task = new ReadWasmProps
+ {
+ BuildEngine = new TestBuildEngine(),
+ JsonFilePath = jsonPath,
+ };
+
+ Assert.True(task.Execute());
+ ITaskItem emccProperty = Assert.Single(task.EmccProperties!);
+ Assert.Equal("RuntimeEmccVersion", emccProperty.ItemSpec);
+ Assert.Equal("4.0.1", emccProperty.GetMetadata("Value"));
+ Assert.Collection(
+ task.WasmOptConfigurationFlags!,
+ item => Assert.Equal("--enable-simd", item.ItemSpec),
+ item =>
+ {
+ Assert.Equal("--enable-threads", item.ItemSpec);
+ Assert.Equal("runtime-pack", item.GetMetadata("Source"));
+ });
+ Assert.Null(task.EmccDefaultExportedFunctions);
+ Assert.Empty(task.EmccDefaultExportedRuntimeMethods!);
+ ITaskItem relinkingProperty = Assert.Single(task.PropertiesThatTriggerRelinking!);
+ Assert.Equal("InvariantGlobalization", relinkingProperty.ItemSpec);
+ Assert.Equal("false", relinkingProperty.GetMetadata("defaultValueInRuntimePack"));
+ }
+
+ [Fact]
+ public void ComponentManifestReaderPreservesItemsAndRuntimeIdentifiers()
+ {
+ using var directory = new TempDirectory();
+ string jsonPath = directory.WriteFile("input.json", JsonWithAllOutputs);
+ var task = new MonoRuntimeComponentManifestReadTask
+ {
+ BuildEngine = new TestBuildEngine(),
+ JsonFilePath = jsonPath,
+ };
+
+ Assert.True(task.Execute());
+ AssertItem(task._MonoRuntimeComponentSharedLibExt, ".dll");
+ AssertItem(task._MonoRuntimeComponentStaticLibExt, ".lib");
+ AssertItem(task._MonoRuntimeComponentLinking, "static");
+ AssertItem(task._MonoRuntimeAvailableComponents, "diagnostics_tracing");
+
+ static void AssertItem(ITaskItem[]? items, string expectedIdentity)
+ {
+ ITaskItem item = Assert.Single(items!);
+ Assert.Equal(expectedIdentity, item.ItemSpec);
+ Assert.Equal("win-x64", item.GetMetadata("RuntimeIdentifier"));
+ }
+ }
+
+ [Theory]
+ [InlineData("0")]
+ [InlineData("1")]
+ public async Task CompiledTasksRunInLegacyAndMultithreadedMsbuild(string forceMultithreaded)
+ {
+ using var directory = new TempDirectory();
+ directory.WriteFile("input.json", JsonWithAllOutputs);
+ string assemblyAttribute = new XAttribute("AssemblyFile", typeof(ReadWasmProps).Assembly.Location).ToString();
+ string projectPath = directory.WriteFile(
+ "reader.proj",
+ $$"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+
+ string dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH")
+ ?? Environment.ProcessPath
+ ?? throw new InvalidOperationException("The dotnet host path is unavailable.");
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = dotnetPath,
+ RedirectStandardError = true,
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ WorkingDirectory = directory.Path,
+ };
+ startInfo.ArgumentList.Add("msbuild");
+ startInfo.ArgumentList.Add(projectPath);
+ startInfo.ArgumentList.Add("-target:Run");
+ startInfo.ArgumentList.Add($"-multithreaded:{(forceMultithreaded == "1" ? "true" : "false")}");
+ startInfo.ArgumentList.Add("-nodeReuse:false");
+ startInfo.ArgumentList.Add("-verbosity:minimal");
+ startInfo.Environment["MSBUILDFORCEMULTITHREADED"] = forceMultithreaded;
+ startInfo.Environment["MSBUILDDISABLENODEREUSE"] = "1";
+
+ using Process process = Process.Start(startInfo)
+ ?? throw new InvalidOperationException("Failed to start MSBuild.");
+ Task standardOutputTask = process.StandardOutput.ReadToEndAsync();
+ Task standardErrorTask = process.StandardError.ReadToEndAsync();
+ await process.WaitForExitAsync();
+ string output = await standardOutputTask + await standardErrorTask;
+
+ Assert.True(process.ExitCode == 0, output);
+ Assert.Contains("linking=static|rid=win-x64", output);
+ Assert.Contains("components=diagnostics_tracing|rid=win-x64", output);
+ Assert.Contains("flags=--enable-simd,--enable-threads", output);
+ Assert.Contains("relinking=InvariantGlobalization|default=false", output);
+ Assert.DoesNotContain("Custom TaskFactory", output);
+ }
+
+ [Fact]
+ public void MissingFileReturnsFalseAndLogsItsPath()
+ {
+ var buildEngine = new TestBuildEngine();
+ var task = new ReadWasmProps
+ {
+ BuildEngine = buildEngine,
+ JsonFilePath = "missing-wasm-props.json",
+ };
+
+ Assert.False(task.Execute());
+ Assert.Equal("Could not find JsonFilePath=missing-wasm-props.json", Assert.Single(buildEngine.Errors));
+ }
+
+ [Theory]
+ [InlineData("""{ "items": { "WasmOptConfigurationFlags": [ "" ] } }""")]
+ [InlineData("""{ "items": { "WasmOptConfigurationFlags": [ { "value": "missing identity" } ] } }""")]
+ public void InvalidJsonReturnsFalseAndLogsError(string json)
+ {
+ using var directory = new TempDirectory();
+ var buildEngine = new TestBuildEngine();
+ var task = new ReadWasmProps
+ {
+ BuildEngine = buildEngine,
+ JsonFilePath = directory.WriteFile("input.json", json),
+ };
+
+ Assert.False(task.Execute());
+ Assert.NotEmpty(buildEngine.Errors);
+ }
+
+ [Fact]
+ public void CaseInsensitiveDuplicatePropertyThrows()
+ {
+ using var directory = new TempDirectory();
+ var task = new ReadWasmProps
+ {
+ BuildEngine = new TestBuildEngine(),
+ JsonFilePath = directory.WriteFile(
+ "input.json",
+ """{ "properties": { "Value": "one", "value": "two" } }"""),
+ };
+
+ AggregateException exception = Assert.Throws(() => task.Execute());
+ Assert.IsType(exception.InnerException);
+ }
+
+ private sealed class TempDirectory : IDisposable
+ {
+ public TempDirectory()
+ {
+ Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName());
+ Directory.CreateDirectory(Path);
+ }
+
+ public string Path { get; }
+
+ public string WriteFile(string fileName, string contents)
+ {
+ string path = System.IO.Path.Combine(Path, fileName);
+ File.WriteAllText(path, contents);
+ return path;
+ }
+
+ public void Dispose() => Directory.Delete(Path, recursive: true);
+ }
+
+ private sealed class TestBuildEngine : IBuildEngine
+ {
+ public List Errors { get; } = new();
+
+ public bool ContinueOnError => false;
+
+ public int LineNumberOfTaskNode => 0;
+
+ public int ColumnNumberOfTaskNode => 0;
+
+ public string ProjectFileOfTaskNode => string.Empty;
+
+ public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) =>
+ throw new NotSupportedException();
+
+ public void LogCustomEvent(CustomBuildEventArgs e)
+ {
+ }
+
+ public void LogErrorEvent(BuildErrorEventArgs e) => Errors.Add(e.Message ?? string.Empty);
+
+ public void LogMessageEvent(BuildMessageEventArgs e)
+ {
+ }
+
+ public void LogWarningEvent(BuildWarningEventArgs e)
+ {
+ }
+ }
+}
diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj
index 1d61acd4817efb..2b5da139545780 100644
--- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj
+++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj
@@ -54,6 +54,7 @@
+
diff --git a/src/mono/wasm/build/WasmApp.Common.targets b/src/mono/wasm/build/WasmApp.Common.targets
index d592ec19a2a55e..caef5afd27737d 100644
--- a/src/mono/wasm/build/WasmApp.Common.targets
+++ b/src/mono/wasm/build/WasmApp.Common.targets
@@ -392,16 +392,7 @@
-
-
-
-
-
-
-
-
-
+
diff --git a/src/tasks/MonoTargetsTasks/JsonToItems/JsonToItemsReader.cs b/src/tasks/MonoTargetsTasks/JsonToItems/JsonToItemsReader.cs
new file mode 100644
index 00000000000000..76dc9a687be8ee
--- /dev/null
+++ b/src/tasks/MonoTargetsTasks/JsonToItems/JsonToItemsReader.cs
@@ -0,0 +1,203 @@
+// 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.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+internal static class JsonToItemsReader
+{
+ private static readonly JsonSerializerOptions s_jsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ AllowTrailingCommas = true,
+ };
+
+ public static bool TryRead(string? jsonFilePath, TaskLoggingHelper log, [NotNullWhen(true)] out JsonModelRoot? json)
+ {
+ json = null;
+
+ if (jsonFilePath is null)
+ {
+ log.LogError("no JsonFilePath specified");
+ return false;
+ }
+
+ if (!File.Exists(jsonFilePath))
+ {
+ log.LogError($"Could not find JsonFilePath={jsonFilePath}");
+ return false;
+ }
+
+ FileStream? file = null;
+ try
+ {
+ try
+ {
+ file = File.OpenRead(jsonFilePath);
+ }
+ catch (FileNotFoundException exception)
+ {
+ log.LogErrorFromException(exception);
+ return false;
+ }
+
+ json = GetJsonAsync(jsonFilePath, file, log).Result;
+ if (json is null)
+ {
+ if (!log.HasLoggedErrors)
+ {
+ log.LogError($"Failed to deserialize json from file {jsonFilePath}");
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+ finally
+ {
+ file?.Dispose();
+ }
+ }
+
+ private static async Task GetJsonAsync(string jsonFilePath, FileStream file, TaskLoggingHelper log)
+ {
+ try
+ {
+ return await JsonSerializer.DeserializeAsync(file, s_jsonOptions).ConfigureAwait(false);
+ }
+ catch (JsonException exception)
+ {
+ log.LogError($"Failed to deserialize json from file '{jsonFilePath}', JSON Path: {exception.Path}, Line: {exception.LineNumber}, Position: {exception.BytePositionInLine}");
+ log.LogErrorFromException(exception, showStackTrace: false, showDetail: true, file: null);
+ return null;
+ }
+ }
+}
+
+internal sealed class JsonModelRoot
+{
+ [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))]
+ public Dictionary? Properties { get; set; }
+
+ public Dictionary? Items { get; set; }
+
+ public ITaskItem[]? GetItems(string name)
+ {
+ if (Items is null || !Items.TryGetValue(name, out JsonModelItem[]? itemModels))
+ {
+ return null;
+ }
+
+ var items = new ITaskItem[itemModels.Length];
+ for (int i = 0; i < itemModels.Length; i++)
+ {
+ JsonModelItem itemModel = itemModels[i];
+ var item = new TaskItem(itemModel.Identity);
+ if (itemModel.Metadata is not null)
+ {
+ foreach (KeyValuePair metadata in itemModel.Metadata)
+ {
+ item.SetMetadata(metadata.Key, metadata.Value);
+ }
+ }
+
+ items[i] = item;
+ }
+
+ return items;
+ }
+}
+
+[JsonConverter(typeof(JsonModelItemConverter))]
+internal sealed class JsonModelItem
+{
+ public JsonModelItem(string identity, Dictionary? metadata)
+ {
+ Identity = identity;
+ Metadata = metadata;
+ }
+
+ public string Identity { get; }
+
+ public Dictionary? Metadata { get; }
+}
+
+internal sealed class CaseInsensitiveDictionaryConverter : JsonConverter>
+{
+ public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ Dictionary? dictionary = JsonSerializer.Deserialize>(ref reader, options);
+ if (dictionary is null)
+ {
+ return null!;
+ }
+
+ return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public override void Write(Utf8JsonWriter writer, Dictionary? value, JsonSerializerOptions options) =>
+ JsonSerializer.Serialize(writer, value, options);
+}
+
+internal sealed class JsonModelItemConverter : JsonConverter
+{
+ public override JsonModelItem Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.String:
+ string? stringItem = reader.GetString();
+ if (stringItem is null || stringItem.Length == 0)
+ {
+ throw new JsonException("deserialized json string item was null or the empty string");
+ }
+
+ return new JsonModelItem(stringItem, metadata: null);
+
+ case JsonTokenType.StartObject:
+ Dictionary? dictionary = JsonSerializer.Deserialize>(ref reader, options);
+ if (dictionary is null)
+ {
+ return null!;
+ }
+
+ var caseInsensitiveDictionary = new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase);
+ if (!caseInsensitiveDictionary.TryGetValue("Identity", out string? identity))
+ {
+ throw new JsonException("deserialized json dictionary item did not have a non-empty Identity metadata");
+ }
+
+ if (identity is null || identity.Length == 0)
+ {
+ throw new JsonException("deserialized json dictionary item did not have a non-empty Identity metadata");
+ }
+
+ caseInsensitiveDictionary.Remove("Identity");
+ return new JsonModelItem(identity, caseInsensitiveDictionary);
+
+ default:
+ throw new NotSupportedException();
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, JsonModelItem value, JsonSerializerOptions options)
+ {
+ if (value.Metadata is null)
+ {
+ JsonSerializer.Serialize(writer, value.Identity);
+ }
+ else
+ {
+ JsonSerializer.Serialize(writer, value.Metadata);
+ }
+ }
+}
diff --git a/src/tasks/MonoTargetsTasks/JsonToItems/JsonToItemsTasks.cs b/src/tasks/MonoTargetsTasks/JsonToItems/JsonToItemsTasks.cs
new file mode 100644
index 00000000000000..6be0c0eedf66c5
--- /dev/null
+++ b/src/tasks/MonoTargetsTasks/JsonToItems/JsonToItemsTasks.cs
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Build.Framework;
+
+public sealed class MonoRuntimeComponentManifestReadTask : Microsoft.Build.Utilities.Task
+{
+ private JsonModelRoot? _json;
+
+ [Required]
+ public string? JsonFilePath { get; set; }
+
+ [Output]
+ public ITaskItem[]? _MonoRuntimeComponentSharedLibExt => _json?.GetItems(nameof(_MonoRuntimeComponentSharedLibExt));
+
+ [Output]
+ public ITaskItem[]? _MonoRuntimeComponentStaticLibExt => _json?.GetItems(nameof(_MonoRuntimeComponentStaticLibExt));
+
+ [Output]
+ public ITaskItem[]? _MonoRuntimeComponentLinking => _json?.GetItems(nameof(_MonoRuntimeComponentLinking));
+
+ [Output]
+ public ITaskItem[]? _MonoRuntimeAvailableComponents => _json?.GetItems(nameof(_MonoRuntimeAvailableComponents));
+
+ public override bool Execute()
+ {
+ return JsonToItemsReader.TryRead(JsonFilePath, Log, out _json);
+ }
+}
+
+public sealed class ReadWasmProps : Microsoft.Build.Utilities.Task
+{
+ private JsonModelRoot? _json;
+
+ [Required]
+ public string? JsonFilePath { get; set; }
+
+ [Output]
+ public ITaskItem[]? EmccProperties => _json?.GetItems(nameof(EmccProperties));
+
+ [Output]
+ public ITaskItem[]? WasmOptConfigurationFlags => _json?.GetItems(nameof(WasmOptConfigurationFlags));
+
+ [Output]
+ public ITaskItem[]? EmccDefaultExportedFunctions => _json?.GetItems(nameof(EmccDefaultExportedFunctions));
+
+ [Output]
+ public ITaskItem[]? EmccDefaultExportedRuntimeMethods => _json?.GetItems(nameof(EmccDefaultExportedRuntimeMethods));
+
+ [Output]
+ public ITaskItem[]? PropertiesThatTriggerRelinking => _json?.GetItems(nameof(PropertiesThatTriggerRelinking));
+
+ public override bool Execute()
+ {
+ return JsonToItemsReader.TryRead(JsonFilePath, Log, out _json);
+ }
+}
diff --git a/src/tasks/MonoTargetsTasks/JsonToItems/README.md b/src/tasks/MonoTargetsTasks/JsonToItems/README.md
new file mode 100644
index 00000000000000..3c7a2a3a1be0bf
--- /dev/null
+++ b/src/tasks/MonoTargetsTasks/JsonToItems/README.md
@@ -0,0 +1,30 @@
+# JSON manifest reader tasks
+
+`MonoTargetsTasks.dll` contains two compiled MSBuild tasks that read JSON manifests into fixed sets of item outputs:
+
+- `MonoRuntimeComponentManifestReadTask` reads runtime component definitions.
+- `ReadWasmProps` reads browser and WASI runtime-pack settings.
+
+Both tasks take a required `JsonFilePath` parameter. The JSON document has an optional top-level `properties` object and an optional `items` object. Each key in `items` names an output, whose value is an array containing either strings or objects:
+
+```json
+{
+ "items": {
+ "WasmOptConfigurationFlags": [
+ "--enable-simd",
+ {
+ "identity": "--enable-threads",
+ "source": "runtime-pack"
+ }
+ ]
+ }
+}
+```
+
+A string is used as the item identity. An object must contain a non-empty `Identity` value; its other string values become item metadata. Property names, `Identity`, and metadata names are matched case-insensitively. Item-group names are case-sensitive. Trailing commas are accepted, but comments are not.
+
+Register the compiled task directly:
+
+```xml
+
+```
diff --git a/src/tasks/MonoTargetsTasks/JsonToItemsTaskFactory/JsonToItemsTaskFactory.cs b/src/tasks/MonoTargetsTasks/JsonToItemsTaskFactory/JsonToItemsTaskFactory.cs
deleted file mode 100644
index 77d29123742a73..00000000000000
--- a/src/tasks/MonoTargetsTasks/JsonToItemsTaskFactory/JsonToItemsTaskFactory.cs
+++ /dev/null
@@ -1,392 +0,0 @@
-// 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.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Threading.Tasks;
-
-using Microsoft.Build.Framework;
-using Microsoft.Build.Utilities;
-
-namespace JsonToItemsTaskFactory
-{
-
- /// Reads a json input blob and populates some output items
- ///
- /// JSON should follow this structure - the toplevel "properties" and "items" keys are exact, other keys are arbitrary.
- ///
- /// {
- /// "properties" : {
- /// "propName1": "value1",
- /// "propName2": "value"
- /// },
- /// "items" : {
- /// "itemName1": [ "stringValue", { "identity": "anotherValue", "metadataKey": "metadataValue", ... }, "thirdValue" ],
- /// "itemName2": [ ... ]
- /// }
- ///
- ///
- /// A task can be declared by
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- /// And then used in a target. The `JsonFilePath' attribute is used to specify the json file to read.
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public class JsonToItemsTaskFactory : ITaskFactory
- {
- private const string JsonFilePath = "JsonFilePath";
- private TaskPropertyInfo[]? _taskProperties;
- private string? _taskName;
-
- private bool _logDebugTask;
-
- public JsonToItemsTaskFactory() {}
-
- public string FactoryName => "JsonToItemsTaskFactory";
-
- public Type TaskType => typeof(JsonToItemsTask);
-
- public bool Initialize(string taskName, IDictionary parameterGroup, string? taskBody, IBuildEngine taskFactoryLoggingHost)
- {
- _taskName = taskName;
- if (taskBody != null && taskBody.StartsWith("debug", StringComparison.InvariantCultureIgnoreCase))
- _logDebugTask = true;
- var log = new TaskLoggingHelper(taskFactoryLoggingHost, _taskName);
- if (!ValidateParameterGroup (parameterGroup, log))
- return false;
- _taskProperties = new TaskPropertyInfo[parameterGroup.Count + 1];
- _taskProperties[0] = new TaskPropertyInfo(nameof(JsonFilePath), typeof(string), output: false, required: true);
- parameterGroup.Values.CopyTo(_taskProperties, 1);
- return true;
- }
-
- public TaskPropertyInfo[] GetTaskParameters() => _taskProperties!;
-
- public ITask CreateTask(IBuildEngine taskFactoryLoggingHost)
- {
- var log = new TaskLoggingHelper(taskFactoryLoggingHost, _taskName);
- if (_logDebugTask) log.LogMessage(MessageImportance.Low, "CreateTask called");
- return new JsonToItemsTask(_taskName!, _logDebugTask);
- }
-
- public void CleanupTask(ITask task) {}
-
- internal bool ValidateParameterGroup(IDictionary parameterGroup, TaskLoggingHelper log)
- {
- var taskName = _taskName ?? "";
- foreach (var kvp in parameterGroup)
- {
- var propName = kvp.Key;
- var propInfo = kvp.Value;
- if (string.Equals(propName, nameof(JsonFilePath), StringComparison.InvariantCultureIgnoreCase))
- {
- log.LogError($"Task {taskName}: {nameof(JsonFilePath)} parameter must not be declared. It is implicitly added by the task.");
- continue;
- }
-
- if (!propInfo.Output)
- {
- log.LogError($"Task {taskName}: parameter {propName} is not an output. All parameters except {nameof(JsonFilePath)} must be outputs");
- continue;
- }
- if (propInfo.Required)
- {
- log.LogError($"Task {taskName}: parameter {propName} is an output but is marked required. That's not supported.");
- }
- if (typeof(ITaskItem[]).IsAssignableFrom(propInfo.PropertyType))
- continue; // ok, an item list
- if (typeof(string).IsAssignableFrom(propInfo.PropertyType))
- continue; // ok, a string property
-
- log.LogError($"Task {taskName}: parameter {propName} is not an output of type System.String or Microsoft.Build.Framework.ITaskItem[]");
- }
- return !log.HasLoggedErrors;
- }
-
- public class JsonToItemsTask : IGeneratedTask
- {
- private IBuildEngine? _buildEngine;
- public IBuildEngine BuildEngine { get => _buildEngine!; set { _buildEngine = value; SetBuildEngine(value);} }
- public ITaskHost? HostObject { get; set; }
-
- private TaskLoggingHelper? _log;
- private TaskLoggingHelper Log { get => _log!; set { _log = value; } }
-
- private void SetBuildEngine(IBuildEngine buildEngine)
- {
- Log = new TaskLoggingHelper(buildEngine, TaskName);
- }
-
- public static JsonSerializerOptions JsonOptions => new()
- {
- PropertyNameCaseInsensitive = true,
- AllowTrailingCommas = true,
- };
- private string? jsonFilePath;
-
- private readonly bool _logDebugTask; // print stuff to the log for debugging the task
-
- private JsonModelRoot? jsonModel;
- public string TaskName {get;}
- public JsonToItemsTask(string taskName, bool logDebugTask = false)
- {
- TaskName = taskName;
- _logDebugTask = logDebugTask;
- }
-
- public bool Execute()
- {
- if (jsonFilePath == null)
- {
- Log.LogError($"no {nameof(JsonFilePath)} specified");
- return false;
- }
- if (!File.Exists(jsonFilePath))
- {
- Log.LogError($"Could not find {nameof(JsonFilePath)}={jsonFilePath}");
- return false;
- }
-
- if (!TryGetJson(jsonFilePath, out var json))
- return false;
-
- if (_logDebugTask)
- {
- LogParsedJson(json);
- }
- jsonModel = json;
- return true;
- }
-
- public bool TryGetJson(string jsonFilePath, [NotNullWhen(true)] out JsonModelRoot? json)
- {
- FileStream? file = null;
- try
- {
- try
- {
- file = File.OpenRead(jsonFilePath);
- }
- catch (FileNotFoundException fnfe)
- {
- Log.LogErrorFromException(fnfe);
- json = null;
- return false;
- }
- json = GetJsonAsync(jsonFilePath, file).Result;
- if (json == null)
- {
- // the async task may have already caught an exception and logged it.
- if (!Log.HasLoggedErrors) Log.LogError($"Failed to deserialize json from file {jsonFilePath}");
- return false;
- }
- return true;
- }
- finally
- {
- file?.Dispose();
- }
- }
-
- public async Task GetJsonAsync(string jsonFilePath, FileStream file)
- {
- JsonModelRoot? json = null;
- try
- {
- json = await JsonSerializer.DeserializeAsync(file, JsonOptions).ConfigureAwait(false);
- }
- catch (JsonException e)
- {
- Log.LogError($"Failed to deserialize json from file '{jsonFilePath}', JSON Path: {e.Path}, Line: {e.LineNumber}, Position: {e.BytePositionInLine}");
- Log.LogErrorFromException(e, showStackTrace: false, showDetail: true, file: null);
- }
- return json;
- }
-
- internal void LogParsedJson (JsonModelRoot json)
- {
- if (json.Properties != null)
- {
- Log.LogMessage(MessageImportance.Low, "json has properties: ");
- foreach (var property in json.Properties)
- {
- Log.LogMessage(MessageImportance.Low, $" {property.Key} = {property.Value}");
- }
- }
- if (json.Items != null)
- {
- Log.LogMessage(MessageImportance.Low, "items: ");
- foreach (var item in json.Items)
- {
- Log.LogMessage(MessageImportance.Low, $" {item.Key} = [");
- foreach (var value in item.Value)
- {
- Log.LogMessage(MessageImportance.Low, $" {value.Identity}");
- if (value.Metadata != null)
- {
- Log.LogMessage(MessageImportance.Low, " and some metadata, too");
- }
- }
- Log.LogMessage(MessageImportance.Low, " ]");
- }
- }
- }
-
- public object? GetPropertyValue(TaskPropertyInfo property)
- {
- bool isItem = false;
- if (typeof(ITaskItem[]).IsAssignableFrom(property.PropertyType))
- {
- if (_logDebugTask) Log.LogMessage(MessageImportance.Low, "GetPropertyValue called with @({0})", property.Name);
- isItem = true;
- }
- else
- {
- if (_logDebugTask) Log.LogMessage(MessageImportance.Low, "GetPropertyValue called with $({0})", property.Name);
- }
- if (!isItem)
- {
- if (jsonModel?.Properties != null && jsonModel.Properties.TryGetValue(property.Name, out var value))
- {
- return value;
- }
- Log.LogError("Property {0} not found in {1}", property.Name, jsonFilePath);
- throw new Exception();
- }
- else
- {
- if (jsonModel?.Items != null && jsonModel.Items.TryGetValue(property.Name, out var itemModels))
- {
- return ConvertItems(itemModels);
- }
-
- }
- return null;
- }
-
- public static ITaskItem[] ConvertItems(JsonModelItem[] itemModels)
- {
- var items = new ITaskItem[itemModels.Length];
- for (int i = 0; i < itemModels.Length; i++)
- {
- var itemModel = itemModels[i];
- var item = new TaskItem(itemModel.Identity);
- if (itemModel.Metadata != null)
- {
- // assume Identity key was already removed in JsonModelItem
- foreach (var metadata in itemModel.Metadata)
- {
- item.SetMetadata(metadata.Key, metadata.Value);
- }
- }
- items[i] = item;
- }
- return items;
- }
-
- public void SetPropertyValue(TaskPropertyInfo property, object? value)
- {
- if (_logDebugTask) Log.LogMessage(MessageImportance.Low, "SetPropertyValue called with {0}", property.Name);
- if (property.Name == "JsonFilePath")
- {
- jsonFilePath = (string)value!;
- }
- else
- throw new Exception($"JsonToItemsTask {TaskName} cannot set property {property.Name}");
- }
-
- }
-
- public class JsonModelRoot
- {
- [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))]
- public Dictionary? Properties {get; set;}
- public Dictionary? Items {get; set;}
-
- public JsonModelRoot() {}
- }
-
- [JsonConverter(typeof(JsonModelItemConverter))]
- public class JsonModelItem
- {
- public string Identity {get;}
- // n.b. will be deserialized case insensitive
- public Dictionary? Metadata {get;}
-
- public JsonModelItem(string identity, Dictionary? metadata)
- {
- Identity = identity;
- Metadata = metadata;
- }
- }
-
- public class CaseInsensitiveDictionaryConverter : JsonConverter>
- {
- public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- var dict = JsonSerializer.Deserialize>(ref reader, options);
- if (dict == null)
- return null!;
- return new Dictionary(dict, StringComparer.OrdinalIgnoreCase);
- }
- public override void Write(Utf8JsonWriter writer, Dictionary? value, JsonSerializerOptions options) =>
- JsonSerializer.Serialize(writer, value, options);
- }
- public class JsonModelItemConverter : JsonConverter
- {
- public JsonModelItemConverter() {}
-
- public override JsonModelItem Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- switch (reader.TokenType)
- {
- case JsonTokenType.String:
- var stringItem = reader.GetString();
- if (string.IsNullOrEmpty(stringItem))
- throw new JsonException ("deserialized json string item was null or the empty string");
- return new JsonModelItem(stringItem!, metadata: null);
- case JsonTokenType.StartObject:
- var dict = JsonSerializer.Deserialize>(ref reader, options);
- if (dict == null)
- return null!;
- var idict = new Dictionary(dict, StringComparer.OrdinalIgnoreCase);
- if (!idict.TryGetValue("Identity", out var identity) || string.IsNullOrEmpty(identity))
- throw new JsonException ("deserialized json dictionary item did not have a non-empty Identity metadata");
- else
- idict.Remove("Identity");
- return new JsonModelItem(identity, metadata: idict);
- default:
- throw new NotSupportedException();
- }
- }
- public override void Write(Utf8JsonWriter writer, JsonModelItem value, JsonSerializerOptions options)
- {
- if (value.Metadata == null)
- JsonSerializer.Serialize(writer, value.Identity);
- else
- JsonSerializer.Serialize(writer, value.Metadata); /* assumes Identity is in there */
- }
- }
- }
-}
diff --git a/src/tasks/MonoTargetsTasks/JsonToItemsTaskFactory/README.md b/src/tasks/MonoTargetsTasks/JsonToItemsTaskFactory/README.md
deleted file mode 100644
index 80d64e940fd009..00000000000000
--- a/src/tasks/MonoTargetsTasks/JsonToItemsTaskFactory/README.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# JsonToItemsTaskFactory
-
-A utility for reading json blobs into MSBuild items and properties.
-
-## Json blob format
-
-The json data must be a single toplevel dictionary with a `"properties"` or an `"items"` key (both are optional).
-
-The `"properties"` value must be a dictionary with more string values. The keys are case-insensitive and duplicates are not allowed.
-
-The `"items"` value must be an array of either string or dictionary elements (or a mix of both).
-String elements use the string value as the `Identity`.
-Dictionary elements must have strings as values, and must include an `"Identity"` key, and as many other metadata key/value pairs as desired. This dictionary is also case-insensitive and duplicate metadata keys are also not allowed.
-
-#### Example
-
-```json
-{
- "properties": {
- "x1": "val1",
- "X2": "val2",
- },
- "items" : {
- "FunFiles": ["funFile1.txt", "funFile2.txt"],
- "FilesWithMeta": [{"identity": "funFile3.txt", "TargetPath": "bin/fun3"},
- "funFile3.and.a.half.txt",
- {"identity": "funFile4.txt", "TargetPath": "bin/fun4"}]
- }
-}
-```
-
-## UsingTask and Writing Targets
-
-To use the task, you need to reference the assembly and add the task to the project, as well as declare the task parameters that correspond to the properties and items you want to retrieve from the json blob.
-
-```xml
-
-
-
-
-
-
-
-```
-
-The parameter group parameters are all optional. They must be non-required outputs of type `System.String` or `Microsoft.Build.Framework.ITaskItem[]`. The former declares properties to capture from the file, while the latter declares item lists.
-
-The above declares a task `MyJsonReader` which will be used to retries the `X1` property and the `FunFiles` and `FilesWithMeta` items.
-
-To use the task, a `JsonFilePath` attribute specifies the file to read.
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-When the target `RunMe` runs, the task will read the json file and populate the outputs. Running the target, the output will be:
-
-```console
-$ dotnet build Example
- X1 = val1
- FunFiles = funFile1.txt;funFile2.txt
- FilesWithMeta = funFile3.txt TargetPath='bin/fun3'
- FilesWithMeta = funFile4.txt TargetPath='bin/fun4'
- FilesWithMeta = funFile3.and.a.half.txt (No TargetPath)
-
-Build succeeded.
- 0 Warning(s)
- 0 Error(s)
-Time Elapsed 00:00:00.15
-```
-