[mono] new Sdk that selects mono runtime components (#54432)
authorAleksey Kliger (λgeek) <alklig@microsoft.com>
Thu, 8 Jul 2021 00:49:33 +0000 (20:49 -0400)
committerGitHub <noreply@github.com>
Thu, 8 Jul 2021 00:49:33 +0000 (00:49 +0000)
* Work in progress: new Sdk that selects mono runtime components

* Add props and targets description to the components doc

* condition the _MonoRuntimeAvailableComponents by RuntimeIdentifier

* [cmake] Write a component-manifest.props file during build

If we're not building a mono aot cross compiler, generate a
component-manifest.props file in artifacts/obj/mono/<host>/ that indicates if
the host will use static or dynamic components, and a list of the available
components, and the properties for constructing their names.

* Build Microsoft.NETCore.App.Runtime.Mono.<RID>.Sdk shared framework nuget

It seems to also generate a symbols nuget.  And in the nuget there's a
tools/mono-sdk-what-is-this.deps.json file from the
SharedFrameworkHostFileNameOverride property.  It would be nice to exclude that
stuff.

* put the compoonent-manifest.targets into the Sdk

* delete WIP in mono/nuget/

* fixup static component names in component-manifest.targets

* delete fixed fixme

* add missing $

* fix whitespace

* [cmake] switch to configure_file instead of file(CONFIGURE)

* add missing trailing slashes in .props.in file

* Add new Sdk packs to the workload manifest

* rework component-manifest.targets to use ItemGroups; move to new SDK

* Rename shared framework to Microsoft.NETCore.App.Runtime.Mono.<RID>.Props.Sdk

And only include component-manifest.props, not the targets

* Update manifest to include the new Props.Sdk and MonoTargets.Sdk

* Move RuntimeConfigParserTask into Microsoft.NET.Runtime.MonoTargets.Sdk

Consolidate all platform-independent tasks and targets into a single Sdk

* Add iossimulator-x86 props

* update components design doc

* Fix typo

* improve docs

* Add _MonoRuntimeComponentDontLink target output

* Drop component-manifest.props into runtime pack build/ directory

Remove from the Microsoft.NETCore.App.Mono.Props.Sdk workload nuget

* remove Microsoft.NETCore.App.Mono.Props.Sdk

* Import component-manifest.props from the runtime pack

Move the MonoTargets.Sdk import to each target platform that we support

* Fix typos

Co-authored-by: Rolf Bjarne Kvinge <rolf@xamarin.com>
* Apply suggestions from code review

Co-authored-by: Ankit Jain <radical@gmail.com>
* Add JsonToItemsTaskFactory

* fix whitespace

* Do some validation earlier in _MonoComputeAvailableComponentDefinitions

* Read component-manifest.json using the JsonToItemsTaskFactory

and bundle it in Microsoft.NET.Runtime.MonoTargets.Sdk

* remove ResolvedRuntimePack import from WorkloadManifest.targets

it's too early, and we have the JsonToItemsTaskFactory now to read the manifest

* Generate component-manifest.json in CMakeLists.txt

* Fix some copy-paste nits

* Use RuntimeFlavor==mono for runtime pack build directory

instead of TargetsMobile.  We want the build files (mono-components.json)
in every mono runtime pack, not just on mobile targets

* Apply suggestions from code review

Co-authored-by: Ankit Jain <radical@gmail.com>
* rename component-manifest to RuntimeComponentManifest

* fixup nullability annotations

* fix whitespace

* fix formatting

* Misc fixes to JsonToItemsTaskFactory

* Rename MonoRuntimeComponentManifestReadTask

from MonoRuntimeComponentsReadManifestTask

* undo nullability annotation

Build doesn't like it for some reason (probably net472)

* fix incorrect task parameter name

* Remove Identity metadata from dictionary at json parsing time

Also improve nullability a bit by making the properties immutable

* Throw correct json deserializer exceptions

* Catch JsonException in an async function

We get nice error messages now like

```
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.targets(8,5): error : Failed to deserialize json from file 'artifacts/bin/mono/iOSSimulator.x64.Release/build/RuntimeComponentManifest.json', JSON Path: $.items._MonoRuntimeAvailableComponents[2], Line: 14, Position: 1 [component-manifest.sample.proj]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.targets(8,5): error : JsonException: The JSON value could not be converted to System.Collections.Generic.Dictionary`2[System.String,System.String]. Path: $.identity | LineNumber: 0 | BytePositionInLine: 16. Path: $.items._MonoRuntimeAvailableComponents[2] | LineNumber: 14 | BytePositionInLine: 1. [component-manifest.sample.proj]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.targets(8,5): error : InvalidOperationException: Cannot get the value of a token type 'Number' as a string. [component-manifest.sample.proj]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.targets(8,5):
error :
[component-manifest.sample.proj]
```

* fixup comments

Co-authored-by: Rolf Bjarne Kvinge <rolf@xamarin.com>
Co-authored-by: Ankit Jain <radical@gmail.com>
21 files changed:
docs/design/mono/components.md
src/installer/pkg/sfx/Microsoft.NETCore.App/Microsoft.NETCore.App.Runtime.props
src/mono/mono.proj
src/mono/mono/component/CMakeLists.txt
src/mono/mono/component/RuntimeComponentManifest.json.in [new file with mode: 0644]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Microsoft.NET.Runtime.MonoTargets.Sdk.pkgproj [new file with mode: 0644]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/README.md [new file with mode: 0644]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.props [new file with mode: 0644]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.targets [new file with mode: 0644]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeConfigParserTask.props [moved from src/mono/nuget/Microsoft.NET.Runtime.RuntimeConfigParser.Task/Sdk/Sdk.props with 100% similarity]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/Sdk.props [new file with mode: 0644]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/Sdk.targets [new file with mode: 0644]
src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/build/Microsoft.NET.Runtime.MonoTargets.Sdk.props [moved from src/mono/nuget/Microsoft.NET.Runtime.RuntimeConfigParser.Task/build/Microsoft.NET.Runtime.RuntimeConfigParser.Task.props with 100% similarity]
src/mono/nuget/Microsoft.NET.Runtime.RuntimeConfigParser.Task/Microsoft.NET.Runtime.RuntimeConfigParser.Task.pkgproj [deleted file]
src/mono/nuget/Microsoft.NET.Runtime.RuntimeConfigParser.Task/README.md [deleted file]
src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.json.in
src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.targets
src/mono/nuget/mono-packages.proj
src/tasks/JsonToItemsTaskFactory/JsonToItemsTaskFactory.cs [new file with mode: 0644]
src/tasks/JsonToItemsTaskFactory/JsonToItemsTaskFactory.csproj [new file with mode: 0644]
src/tasks/JsonToItemsTaskFactory/README.md [new file with mode: 0644]

index 2dc982d..ef17836 100644 (file)
@@ -164,10 +164,10 @@ To implement `feature_X` as a component.  Carry out the following steps:
          { feature_X_cleanup },
          feature_X_hello,
     };
-       
+
        MonoComponentFeatureX *
        mono_component_feature_X_init (void) { return &fn_table; }
-       
+
        void feature_X_cleanup (MonoComponent *self)
        {
          static int cleaned = 0;
@@ -207,10 +207,10 @@ To implement `feature_X` as a component.  Carry out the following steps:
          { feature_X_cleanup },
          feature_X_hello,
     };
-       
+
        MonoComponentFeatureX *
        mono_component_feature_X_init (void) { return &fn_table; }
-       
+
        void feature_X_cleanup (MonoComponent *self)
        {
          static int cleaned = 0;
@@ -229,7 +229,7 @@ To implement `feature_X` as a component.  Carry out the following steps:
   ```c
     MonoComponentFeatureX*
        mono_component_feature_X (void);
-       
+
     ...
     MonoComponentFeatureX*
        mono_component_feature_X_stub_init (void);
@@ -238,7 +238,7 @@ To implement `feature_X` as a component.  Carry out the following steps:
 * Add an entry to the `components` list to load the component to `mono/metadata/components.c`, and also implement the getter for the component:
   ```c
     static MonoComponentFeatureX *feature_X = NULL;
-       
+
     MonoComponentEntry components[] = {
           ...
           {"feature_X", "feature_X", COMPONENT_INIT_FUNC (feature_X), (MonoComponent**)&feature_X, NULL },
@@ -265,16 +265,66 @@ To implement `feature_X` as a component.  Carry out the following steps:
 ## Detailed design - Packaging and runtime packs
 
 The components are building blocks to put together a functional runtime.  The
-runtime pack includes the base runtime and the components and additional
-properties and targets that enable the workload to construct a runtime for
-various scenarios.
-
-In each runtime pack we include:
-
-- The compiled compnents for the apropriate host architectures in a well-known subdirectory
-- An MSBuild props file that defines an item group that list each component name and has metadata that indicates:
-   - the path to the component in the runtime pack
-   - the path to the stub component in the runtime pack (if components are static)
-- An MSBuild targets file that defines targets to copy a specified set of components to the app publish folder (if components are dynamic); or to link the runtime together with stubs and a set of enabled components (if components are static)
-
-** TODO ** Write this up in more detail
+runtime pack includes the base runtime and the components.  The mono workload
+includes the runtime pack and additional tasks, properties and targets that
+enable the workload to construct a runtime for various scenarios.
+
+For the target RID, we expose:
+
+- `@(_MonoRuntimeComponentLinking)` set to either `'static'` or `'dynamic'` depending on whether the
+  current runtime pack for the current target includes runtime components as static archives or as
+  shared libraries, respectively.
+- `@(_MonoRuntimeComponentSharedLibExt)` and `@(_MonoRuntimeComponentStaticLibExt)` set to the file
+  extension of the runtime components for the current target (ie, `'.a', '.so', '.dylib'` etc).
+- `@(_MonoRuntimeAvailableComponents)` a list of component names without the `lib` prefix (if any)
+  or file extensions.  For example: `'hot_reload; diagnostics_tracing'`.
+
+Each of the above item lists has `RuntimeIdentifier` metadata.  For technical reasons the mono
+workload will provide a single `@(_MonoRuntimeAvailableComponent)` item list for all platforms.  We
+use the `RuntimeIdentifier` metadata to filter out the details applicable for the current platform.
+
+- The target `_MonoSelectRuntimeComponents` that has the following inputs and outputs:
+  - input `@(_MonoComponent)` (to be set by the workload) : a list of components that a workload wants to use for the current
+    app.  It is an error if this specifies any unknown component name.
+  - output `@(_MonoRuntimeSelectedComponents)` and `@(_MonoRuntimeSelectedStubComponents)` The names
+    of the components that were (resp, were not) selected.  For example `'hot_reload;
+    diagnostics_tracing'`.  Each item has two metadata properties `ComponentLib` and
+    `ComponentStubLib` (which may be empty) that specify the name of the static or dynamic library
+    of the component.  This is not the main output of the target, it's primarily for debugging.
+  - output `@(_MonoRuntimeComponentLink)` a list of library names (relative to the `native/`
+    subdirectory of the runtime pack) that (for dynamic components) must be placed next to the
+    runtime in the application bundle, or (for static components) that must be linked with the
+    runtime to enable the components' functionality.  Each item in the list has metadata
+    `ComponentName` (e.g. `'hot_reload'`), `IsStub` (`true` or `false`), `Linking` (`'static'` or
+    `'dynamic'`).  This output should be used by the workloads when linking the app and runtime if
+    the workload uses an allow list of native libraries to link or bundle.
+  - output `@(_MonoRuntimeComponentDontLink)` a list of library names (relative to the `native/`
+    subdirectory of the runtime pack) that should be excluded from the application bundle (for
+    dynamic linking) or that should not be passed to the native linker (for static linking).  This
+    output should be used by workloads that just link or bundle every native library from `native/`
+    in order to filter the contents of the subdirectory to exclude the disabled components (and to
+    exclude the static library stubs for the enabled components when static linking).
+
+Generally workloads should only use one of `@(_MonoRuntimeComponentLink)` or
+`@(_MonoRuntimeComponentDontLink)`, depending on whether they use an allow or block list for the
+contents of the `native/` subdirectory.
+
+Example fragment (assuming the mono workload has been imported):
+
+```xml
+  <Project>
+    <ItemGroup Condition="'$(Configuration)' == 'Debug'">
+      <_MonoComponent Include="hot_reload;diagnostics_tracing" />
+    </ItemGroup>
+
+    <Target Name="PrintComponents" DependsOnTargets="_MonoSelectRuntimeComponents">
+      <Message Importance="High" Text="Runtime identifier: $(RuntimeIdentifier)" />
+      <Message Importance="High" Text="Selected : @(_MonoRuntimeSelectedComponents) %(ComponentLib)" />
+      <Message Importance="High" Text="Stubbed out : @(_MonoRuntimeSelectedStubComponents) %(ComponentStubLib)" />
+      <Message Importance="High" Text="Linking with lib @(_MonoRuntimeComponentLink) Stub: %(IsStub) Linking: %(Linking) Component: %(ComponentName)"/>
+
+      <Message Importance="High" Text="UnSelected : @(_MonoRuntimeUnSelectedComponents) %(ComponentLib)" />
+      <Message Importance="High" Text="Exclude these from linking: @(_MonoRuntimeComponentDontLink) Stub: %(IsStub) Linking: %(Linking) Component: %(ComponentName)" />
+    </Target>
+  </Project>
+```
index 2489544..4eea2cf 100644 (file)
         <TargetPath>runtimes/$(RuntimeIdentifier)/native/include/%(RecursiveDir)</TargetPath>
       </RuntimeFiles>
 
+      <RuntimeFiles Condition="'$(RuntimeFlavor)' == 'mono'"
+                    Include="$(MonoArtifactsPath)\build\**\*.*"
+                    ExcludeFromDataFiles="true">
+        <TargetPath>runtimes/$(RuntimeIdentifier)/build/%(RecursiveDir)</TargetPath>
+      </RuntimeFiles>
+
       <CoreCLRCrossTargetFiles PackOnly="true" />
       <CoreCLRCrossTargetFiles Condition="'%(FileName)' == 'clrjit' or '%(FileName)' == 'libclrjit'">
         <TargetPath>runtimes/$(CoreCLRCrossTargetComponentDirName)_$(TargetArchitecture)/native</TargetPath>
index e459f87..c26eb9f 100644 (file)
     <ItemGroup Condition="'$(MonoComponentsStatic)' == 'true'">
       <_MonoCMakeArgs Include="-DSTATIC_COMPONENTS=1" />
     </ItemGroup>
+    <ItemGroup>
+      <_MonoCMakeArgs Include="-DMONO_COMPONENTS_RID=$(TargetOS)-$(TargetArchitecture)" />
+    </ItemGroup>
 
     <PropertyGroup>
       <_MonoCFLAGSOption>-DCMAKE_C_FLAGS="@(_MonoCPPFLAGS, ' ') @(_MonoCFLAGS, ' ')"</_MonoCFLAGSOption>
         <Destination>$(RuntimeBinDir)cross\$(PackageRID)\opt$(ExeExt)</Destination>
       </_MonoRuntimeArtifacts>
       <_MonoIncludeArtifacts Include="$(MonoObjDir)out\include\**" />
+      <_MonoRuntimeBuildArtifacts Include="$(MonoObjDir)\build\**" />
       <_MonoRuntimeArtifacts Condition="'$(_MonoIncludeInterpStaticFiles)' == 'true'" Include="$(MonoObjDir)out\lib\libmono-ee-interp.a">
         <Destination>$(RuntimeBinDir)libmono-ee-interp.a</Destination>
       </_MonoRuntimeArtifacts>
           SkipUnchangedFiles="true"
           Condition="'$(MonoGenerateOffsetsOSGroups)' == '' and ('$(TargetsMacCatalyst)' == 'true' or '$(TargetsiOS)' == 'true' or '$(TargetstvOS)' == 'true' or '$(TargetsAndroid)' == 'true' or '$(TargetsBrowser)' == 'true')"/>
 
+    <Copy SourceFiles="@(_MonoRuntimeBuildArtifacts)"
+          DestinationFiles="@(_MonoRuntimeBuildArtifacts->'$(RuntimeBinDir)build\%(RecursiveDir)%(Filename)%(Extension)')"
+          SkipUnchangedFiles="true"
+          Condition="'$(BuildMonoAOTCrossCompilerOnly)' != 'true'" />
+
     <Exec Condition="'$(BuildMonoAOTCrossCompilerOnly)' != 'true' and '$(MonoGenerateOffsetsOSGroups)' == '' and ('$(TargetsOSX)' == 'true' or '$(TargetsMacCatalyst)' == 'true' or '$(TargetsiOS)' == 'true' or '$(TargetstvOS)' == 'true')" Command="install_name_tool -id @rpath/$(MonoFileName) $(RuntimeBinDir)$(MonoFileName)" />
   </Target>
 
index 62ab5b3..1bc4ead 100644 (file)
@@ -82,6 +82,7 @@ set(${MONO_DIAGNOSTICS_TRACING_COMPONENT_NAME}-dependencies
 
 #define a library for each component and component stub
 function(define_component_libs)
+  # NOTE: keep library naming pattern in sync with RuntimeComponentManifest.targets
   if (NOT DISABLE_LIBS)
     foreach(component IN LISTS components)
       add_library("mono-component-${component}-static" STATIC $<TARGET_OBJECTS:${component}-objects>)
@@ -121,6 +122,7 @@ endforeach()
 if(NOT DISABLE_COMPONENTS AND NOT STATIC_COMPONENTS)
   # define a shared library for each component
   foreach(component IN LISTS components)
+    # NOTE: keep library naming pattern in sync with RuntimeComponentManifest.targets
     if(HOST_WIN32)
       add_library("mono-component-${component}" SHARED "${${component}-sources}")
       target_compile_definitions("mono-component-${component}" PRIVATE -DCOMPILING_COMPONENT_DYNAMIC;-DMONO_DLL_IMPORT)
@@ -172,6 +174,26 @@ foreach(component IN LISTS components)
   list(APPEND mono-components-stub-objects $<TARGET_OBJECTS:${component}-stub-objects>)
 endforeach()
 
+if(NOT MONO_CROSS_COMPILE)
+  set(TemplateMonoRuntimeComponentSharedLibExt "${CMAKE_SHARED_LIBRARY_SUFFIX}")
+  set(TemplateMonoRuntimeComponentStaticLibExt "${CMAKE_STATIC_LIBRARY_SUFFIX}")
+  set(TemplateRuntimeIdentifier "${MONO_COMPONENTS_RID}")
+  if(DISABLE_COMPONENTS)
+    set(TemplateMonoRuntimeComponentLinking "static")
+    set(TemplateMonoRuntimeAvailableComponents "")
+  else()
+    list(TRANSFORM components REPLACE "^(.+)$" "{ \"identity\": \"\\1\", \"RuntimeIdentifier\": \"${TemplateRuntimeIdentifier}\" }," OUTPUT_VARIABLE TemplateMonoRuntimeAvailableComponentsList)
+    list(JOIN TemplateMonoRuntimeAvailableComponentsList "\n" TemplateMonoRuntimeAvailableComponents)
+    if(STATIC_COMPONENTS)
+      set(TemplateMonoRuntimeComponentLinking "static")
+    else()
+      set(TemplateMonoRuntimeComponentLinking "dynamic")
+    endif()
+  endif()
+  # Write a RuntimeComponentManifest.json file in the artifacts/obj/mono/<host>/build/ directory
+  # without the ../.. the file would go in artifacts/obj/mono/<host>/mono/mini
+  configure_file( "${MONO_COMPONENT_PATH}/RuntimeComponentManifest.json.in" "../../build/RuntimeComponentManifest.json")
+endif()
 
 # component tests
 set(MONO_EVENTPIPE_TEST_SOURCE_PATH "${MONO_EVENTPIPE_SHIM_SOURCE_PATH}/test")
diff --git a/src/mono/mono/component/RuntimeComponentManifest.json.in b/src/mono/mono/component/RuntimeComponentManifest.json.in
new file mode 100644 (file)
index 0000000..a7ee6a8
--- /dev/null
@@ -0,0 +1,16 @@
+{
+    "items": {
+        "_MonoRuntimeComponentLinking": [
+            { "identity": "${TemplateMonoRuntimeComponentLinking}", "RuntimeIdentifier": "${TemplateRuntimeIdentifier}" },
+        ],
+        "_MonoRuntimeComponentSharedLibExt": [
+            { "identity": "${TemplateMonoRuntimeComponentSharedLibExt}", "RuntimeIdentifier": "${TemplateRuntimeIdentifier}" },
+        ],
+        "_MonoRuntimeComponentStaticLibExt": [
+            { "identity": "${TemplateMonoRuntimeComponentStaticLibExt}", "RuntimeIdentifier": "${TemplateRuntimeIdentifier}" },
+        ],
+        "_MonoRuntimeAvailableComponents": [
+            ${TemplateMonoRuntimeAvailableComponents}
+        ],
+    }
+}
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Microsoft.NET.Runtime.MonoTargets.Sdk.pkgproj b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Microsoft.NET.Runtime.MonoTargets.Sdk.pkgproj
new file mode 100644 (file)
index 0000000..adf1b80
--- /dev/null
@@ -0,0 +1,23 @@
+<Project DefaultTargets="Build">
+  <Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.props))" />
+
+  <PropertyGroup>
+    <PackageDescription>Provides the tasks+targets, for consumption by mono-based workloads</PackageDescription>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="$(RepoTasksDir)RuntimeConfigParser\RuntimeConfigParser.csproj" />
+    <ProjectReference Include="$(RepoTasksDir)JsonToItemsTaskFactory\JsonToItemsTaskFactory.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageFile Include="Sdk\Sdk.props" TargetPath="Sdk" />
+    <PackageFile Include="Sdk\Sdk.targets" TargetPath="Sdk" />
+    <PackageFile Include="build\$(MSBuildProjectName).props" TargetPath="build" />
+    <PackageFile Include="Sdk\RuntimeConfigParserTask.props" TargetPath="Sdk" />
+    <PackageFile Include="Sdk\RuntimeComponentManifest.props" TargetPath="Sdk" />
+    <PackageFile Include="Sdk\RuntimeComponentManifest.targets" TargetPath="Sdk" />
+  </ItemGroup>
+
+  <Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.targets))" />
+</Project>
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/README.md b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/README.md
new file mode 100644 (file)
index 0000000..9cb7221
--- /dev/null
@@ -0,0 +1,38 @@
+# Mono Runtime Host support targets
+
+This Sdk provides additional tasks and targets for workloads hosting the MonoVM .NET runtime.
+
+## component-manifest.targets
+
+See https://github.com/dotnet/runtime/blob/main/docs/design/mono/components.md
+
+## RuntimeConfigParserTask
+The `RuntimeConfigParserTask` task converts a json `runtimeconfig.json` to a binary blob for MonoVM's `monovm_runtimeconfig_initialize` API.
+To use the task in a project, reference the NuGet package, with the appropriate nuget source.
+
+### NuGet.config
+```xml
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+  <packageSources>
+    <add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
+  </packageSources>
+</configuration>
+```
+
+### In the project file
+```xml
+<!-- Import the NuGet package into the project -->
+<ItemGroup>
+    <PackageReference Include="Microsoft.NET.Runtime.MonoTargets.Sdk" Version="<desired-dotnet-6-sdk-version>" />
+</ItemGroup>
+
+<!-- Use the RuntimeConfigParser task in a target -->
+<Target>
+    <RuntimeConfigParserTask
+        RuntimeConfigFile="$(Path_to_runtimeconfig.json_file)"
+        OutputFile="$(Path_to_generated_binary_file)"
+        RuntimeConfigReservedProperties="@(runtime_properties_reserved_by_host)">
+    </RuntimeConfigParserTask>
+</Target>
+```
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.props b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.props
new file mode 100644 (file)
index 0000000..d173ff6
--- /dev/null
@@ -0,0 +1,14 @@
+<Project>
+  <PropertyGroup>
+    <JsonToItemsTaskFactoryTasksAssemblyPath Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tasks\net6.0\JsonToItemsTaskFactory.dll</JsonToItemsTaskFactoryTasksAssemblyPath>
+    <JsonToItemsTaskFactoryTasksAssemblyPath Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tasks\net472\JsonToItemsTaskFactory.dll</JsonToItemsTaskFactoryTasksAssemblyPath>
+  </PropertyGroup>
+  <UsingTask TaskName="MonoRuntimeComponentManifestReadTask" TaskFactory="JsonToItemsTaskFactory.JsonToItemsTaskFactory" AssemblyFile="$(JsonToItemsTaskFactoryTasksAssemblyPath)">
+    <ParameterGroup>
+      <_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" />
+    </ParameterGroup>
+  </UsingTask>
+</Project>
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.targets b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/RuntimeComponentManifest.targets
new file mode 100644 (file)
index 0000000..411aeec
--- /dev/null
@@ -0,0 +1,112 @@
+<Project>
+  <Target Name="_MonoReadAvailableComponentsManifest">
+    <Error Condition="'%(ResolvedRuntimePack.PackageDirectory)' == '' and '$(_MonoRuntimeComponentManifestJsonFilePath)' == ''"
+           Text="Empty ResolvedRuntimePack.PackageDirectory while trying to read runtime components manifest" />
+    <PropertyGroup>
+      <_MonoRuntimeComponentManifestJsonFilePath Condition="'$(_MonoRuntimeComponentManifestJsonFilePath)' == ''">%(ResolvedRuntimePack.PackageDirectory)\runtimes\$(RuntimeIdentifier)\build\RuntimeComponentManifest.json</_MonoRuntimeComponentManifestJsonFilePath>
+    </PropertyGroup>
+    <MonoRuntimeComponentManifestReadTask JsonFilePath="$(_MonoRuntimeComponentManifestJsonFilePath)">
+      <Output TaskParameter="_MonoRuntimeComponentSharedLibExt" ItemName="_MonoRuntimeComponentSharedLibExt" />
+      <Output TaskParameter="_MonoRuntimeComponentStaticLibExt" ItemName="_MonoRuntimeComponentStaticLibExt" />
+      <Output TaskParameter="_MonoRuntimeComponentLinking" ItemName="_MonoRuntimeComponentLinking" />
+      <Output TaskParameter="_MonoRuntimeAvailableComponents" ItemName="_MonoRuntimeAvailableComponents" />
+    </MonoRuntimeComponentManifestReadTask>
+  </Target>
+
+  <Target Name="_MonoComputeAvailableComponentDefinitions" DependsOnTargets="_MonoReadAvailableComponentsManifest">
+    <!-- Input: _MonoRuntimeAvailableComponents item list: the components provided by the runtime -->
+    <!-- Input: _MonoRuntimeComponentLinking property: either 'dynamic' or 'static'.  Are the components static or shared libraries. -->
+    <!-- Output: _MonoRuntimeComponentName item list: the components provided by the runtime, with ComponentLib and ComponentStubLib metadata that gives the filenames of the components relative to the runtime pack native subdirectory -->
+    <PropertyGroup>
+      <_MonoRuntimeComponentCurrentSharedLibExt>@(_MonoRuntimeComponentSharedLibExt->WithMetadataValue('RuntimeIdentifier', '$(RuntimeIdentifier)'))</_MonoRuntimeComponentCurrentSharedLibExt>
+      <_MonoRuntimeComponentCurrentStaticLibExt>@(_MonoRuntimeComponentStaticLibExt->WithMetadataValue('RuntimeIdentifier', '$(RuntimeIdentifier)'))</_MonoRuntimeComponentCurrentStaticLibExt>
+      <_MonoRuntimeComponentCurrentLinking>@(_MonoRuntimeComponentLinking->WithMetadataValue('RuntimeIdentifier', '$(RuntimeIdentifier)'))</_MonoRuntimeComponentCurrentLinking>
+    </PropertyGroup>
+    <Error Condition="'$(_MonoRuntimeComponentCurrentSharedLibExt)' == '' or '$(_MonoRuntimeComponentCurrentStaticLibExt)' == ''"
+           Text="No _MonoRuntimeComponentSharedLibExt or _MonoRuntimeComponentStaticLibExt for '$(RuntimeIdentifier)'. Malformed runtime pack?" />
+    <Error Condition="'$(_MonoRuntimeComponentCurrentLinking)' != 'dynamic' and '$(_MonoRuntimeComponentCurrentLinking)' != 'static'"
+           Text="Value of _MonoRuntimeComponentLinking for '$(RuntimeIdentifier)' must be 'dynamic' or 'static' it is '$(_MonoRuntimeComponentCurrentLinking)'. Malformed runtime pack?" />
+    <ItemGroup Condition="'$(_MonoRuntimeComponentCurrentLinking)' == 'dynamic'">
+      <_MonoRuntimeComponentName Include="@(_MonoRuntimeAvailableComponents)" Condition="'%(RuntimeIdentifier)' == '$(RuntimeIdentifier)'">
+        <ComponentLib>libmono-component-%(Identity)$(_MonoRuntimeComponentCurrentSharedLibExt)</ComponentLib>
+      </_MonoRuntimeComponentName>
+    </ItemGroup>
+    <ItemGroup Condition="'$(_MonoRuntimeComponentCurrentLinking)' == 'static'">
+      <_MonoRuntimeComponentName Include="@(_MonoRuntimeAvailableComponents)" Condition="'%(RuntimeIdentifier)' == '$(RuntimeIdentifier)'">
+        <ComponentLib>libmono-component-%(Identity)-static$(_MonoRuntimeComponentCurrentStaticLibExt)</ComponentLib>
+        <ComponentStubLib>libmono-component-%(Identity)-stub-static$(_MonoRuntimeComponentCurrentStaticLibExt)</ComponentStubLib>
+      </_MonoRuntimeComponentName>
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_MonoSelectRuntimeComponents" DependsOnTargets="_MonoComputeAvailableComponentDefinitions">
+    <!-- Input: _MonoComponent item list of names of components that will be enabled (to be defined
+         by the workload) -->
+    <!-- Output: _MonoRuntimeSelectedComponents subset of _MonoRuntimeComponentName items that were
+         in _MonoComponent -->
+    <!-- Output: _MonoRuntimeSelectedStubComponents (static linking only) subset of
+         _MonoRuntimeComponentName items that were not in _MonoComponent -->
+    <!-- Output: _MonoRuntimeUnSelectedComponents subset of _MonoRuntimeComponentName items that were not in _MonoComponent. That is, the components that are available but that weren't selected. -->
+    <!-- Output: _MonoRuntimeUnSelectedStubComponents (static linking only) subset of _MonoRuntimeComponentName items that were in _MonoComponent.  That is, components that were selected - so their stubs should not be linked. -->
+    <!-- Output: _MonoRuntimeComponentLink item list of the filenames to link (relative paths
+         relative to the native/ directory of the runtime pack). For dynamic linking, this is only
+         the enabled components.  For static linking, it also includes stubs for the non-selected
+         components -->
+    <!-- Output: _MonoRuntimeComponentDontLink item list of the filename to exclude from linking
+         (relative paths relative to the native/ directory of the runtime pack). For static linking,
+         this is stubs or components that must not be passed to the native linker. For dynamic
+         linking, this is copmonent shared libraries that must not be bundled with the app
+         -->
+    <!-- Generally the workload should only use one of _MonoRuntimeComponentLink or
+         _MonoRuntimeComponentDontLink.  For example, a workload that doesn't understand the
+         contents of native/ in details will generally bundle every native library from native/.
+         Such a workload should then use DontLink to exclude the disabled components.  On the other
+         hand a workload that explicitly picks the native libraries from native/ should use
+         _MonoRuntimeComponentLink to pick just the needed runtime component artifacts. -->
+   
+    <!-- The target also depends on the following items that are provided by the component-manifest.props from the runtime pack -->
+    <!-- _MonoRuntimeComponentLinking item list: either 'dynamic' or 'static' (defined by the runtime for each possible RuntimeIdentifier) -->
+    <!-- _MonoRuntimeComponentName item list: the components for the current RuntimeIdentifier (defined by _MonoComputeAvailableComponentDefinitions above ) -->
+
+    <ItemGroup Condition="'$(_MonoRuntimeComponentCurrentLinking)' == 'dynamic' or '$(_MonoRuntimeComponentCurrentLinking)' == 'static'">
+      <!-- take the intersection of the two item lists. -->
+      <!-- n.b. copies the metadata from _MonoRuntimeComponentName to _MonoRuntimeSelectedComponents -->
+      <_MonoRuntimeComponentNameForRid Include="@(_MonoRuntimeComponentName)" Condition="'%(RuntimeIdentifier)' == '$(RuntimeIdentifier)'" />
+      <_MonoRuntimeSelectedComponents Include="@(_MonoRuntimeComponentNameForRid)" Condition="'@(_MonoRuntimeComponentNameForRid)' == '@(_MonoComponent)' and '%(Identity)' != ''" />
+      <_MonoRuntimeUnSelectedComponents Include="@(_MonoRuntimeComponentNameForRid)" Exclude="@(_MonoComponent)" />
+      <_MonoRuntimeComponentLink Include="%(_MonoRuntimeSelectedComponents.ComponentLib)">
+        <Linking>$(_MonoRuntimeComponentCurrentLinking)</Linking>
+        <IsStub>false</IsStub>
+        <ComponentName>%(_MonoRuntimeSelectedComponents.Identity)</ComponentName>
+      </_MonoRuntimeComponentLink>
+      <_MonoRuntimeComponentDontLink Include="%(_MonoRuntimeUnSelectedComponents.ComponentLib)">
+        <Linking>$(_MonoRuntimeComponentCurrentLinking)</Linking>
+        <IsStub>false</IsStub>
+        <ComponentName>%(_MonoRuntimeUnSelectedComponents.Identity)</ComponentName>
+      </_MonoRuntimeComponentDontLink>
+      <!-- non-empty if any unknown components are specified -->
+      <_MonoRuntimeSelectedMissingComponents Include="@(_MonoComponent)" Exclude="@(_MonoRuntimeComponentNameForRid)" />
+    </ItemGroup>
+
+    <ItemGroup Condition="'$(_MonoRuntimeComponentCurrentLinking)' == 'static'">
+      <!-- Take difference _MonoRuntimeComponentName - _MonoComponent.  The remaining components are stubbed out -->
+      <_MonoRuntimeSelectedStubComponents Include="@(_MonoRuntimeComponentNameForRid)" Exclude="@(_MonoComponent)" />
+      <_MonoRuntimeUnSelectedStubComponents Include="@(_MonoRuntimeComponentNameForRid)" Condition="'@(_MonoRuntimeComponentNameForRid)' == '@(_MonoComponent)' and '%(Identity)' != ''" />
+      <_MonoRuntimeComponentLink Include="%(_MonoRuntimeSelectedStubComponents.ComponentStubLib)">
+        <Linking>$(_MonoRuntimeComponentCurrentLinking)</Linking>
+        <IsStub>true</IsStub>
+        <ComponentName>%(_MonoRuntimeSelectedStubComponents.Identity)</ComponentName>
+      </_MonoRuntimeComponentLink>
+      <_MonoRuntimeComponentDontLink Include="%(_MonoRuntimeUnSelectedStubComponents.ComponentStubLib)">
+        <Linking>$(_MonoRuntimeComponentCurrentLinking)</Linking>
+        <IsStub>true</IsStub>
+        <ComponentName>%(_MonoRuntimeUnSelectedStubComponents.Identity)</ComponentName>
+      </_MonoRuntimeComponentDontLink>
+    </ItemGroup>
+
+    <Error Condition="'$(_MonoRuntimeComponentCurrentLinking)' != 'dynamic' and '$(_MonoRuntimeComponentCurrentLinking)' != 'static'" Text="_MonoRuntimeComponentCurrentLinking is not dynamic or static.  It is '$(_MonoRuntimeComponentCurrentLinking)'.  RuntimeIdentifier is '$(RuntimeIdentifier)'." />
+    <Error Condition="@(_MonoRuntimeSelectedMissingComponents->Count()) != 0"
+             Text="_MonoComponent item list includes components '@(_MonoRuntimeSelectedMissingComponents)' which are not defined in the runtime pack. Runtime pack includes '@(_MonoRuntimeComponentNameForRid)'" />
+
+  </Target>
+</Project>
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/Sdk.props b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/Sdk.props
new file mode 100644 (file)
index 0000000..d292fe1
--- /dev/null
@@ -0,0 +1,4 @@
+<Project>
+  <Import Project="$(MSBuildThisFileDirectory)\RuntimeConfigParserTask.props" />
+  <Import Project="$(MSBuildThisFileDirectory)\RuntimeComponentManifest.props" />
+</Project>
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/Sdk.targets b/src/mono/nuget/Microsoft.NET.Runtime.MonoTargets.Sdk/Sdk/Sdk.targets
new file mode 100644 (file)
index 0000000..5fdf494
--- /dev/null
@@ -0,0 +1,3 @@
+<Project>
+  <Import Project="$(MSBuildThisFileDirectory)\RuntimeComponentManifest.targets" />
+</Project>
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.RuntimeConfigParser.Task/Microsoft.NET.Runtime.RuntimeConfigParser.Task.pkgproj b/src/mono/nuget/Microsoft.NET.Runtime.RuntimeConfigParser.Task/Microsoft.NET.Runtime.RuntimeConfigParser.Task.pkgproj
deleted file mode 100644 (file)
index 9591b89..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<Project DefaultTargets="Build">
-  <Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.props))" />
-
-  <PropertyGroup>
-    <PackageDescription>Provides the RuntimeConfigParser task</PackageDescription>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="$(RepoTasksDir)RuntimeConfigParser\RuntimeConfigParser.csproj" />
-
-    <PackageFile Include="Sdk\Sdk.props" TargetPath="Sdk" />
-    <PackageFile Include="build\$(MSBuildProjectName).props" TargetPath="build" />
-  </ItemGroup>
-
-  <Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.targets))" />
-</Project>
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.RuntimeConfigParser.Task/README.md b/src/mono/nuget/Microsoft.NET.Runtime.RuntimeConfigParser.Task/README.md
deleted file mode 100644 (file)
index 1d55762..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-# RuntimeConfigParser MSBuild Task NuPkg
-The `RuntimeConfigParser` MSBuild task is also useful outside the context of `dotnet/runtime`. The task is made available through a NuGet Package containing the `RuntimeConfigParser.dll` assembly produced from building `RuntimeConfigParser.csproj`. To use the task in a project, reference the NuGet package, with the appropriate nuget source.
-
-## NuGet.config
-```
-<?xml version="1.0" encoding="utf-8"?>
-<configuration>
-  <packageSources>
-    <add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
-  </packageSources>
-</configuration>
-```
-
-## In the project file
-```
-<!-- Import the NuGet package into the project -->
-<ItemGroup>
-    <PackageReference Include="Microsoft.NET.Runtime.RuntimeConfigParser.Task" Version="<desired-dotnet-6-sdk-version>" />
-</ItemGroup>
-
-<!-- Use the RuntimeConfigParser task in a target -->
-<Target>
-    <RuntimeConfigParserTask
-        RuntimeConfigFile="$(Path_to_runtimeconfig.json_file)"
-        OutputFile="$(Path_to_generated_binary_file)"
-        RuntimeConfigReservedProperties="@(runtime_properties_reserved_by_host)">
-    </RuntimeConfigParserTask>
-</Target>
-```
index 059efee..e6a7ff1 100644 (file)
       "abstract": true,
       "description": "Shared native build tooling for Mono runtime",
       "packs": [
-        "Microsoft.NET.Runtime.RuntimeConfigParser.Task",
         "Microsoft.NET.Runtime.MonoAOTCompiler.Task",
+        "Microsoft.NET.Runtime.MonoTargets.Sdk",
       ],
     }
   },
   "packs": {
-    "Microsoft.NET.Runtime.RuntimeConfigParser.Task": {
+    "Microsoft.NET.Runtime.MonoAOTCompiler.Task": {
       "kind": "Sdk",
       "version": "${PackageVersion}"
     },
-    "Microsoft.NET.Runtime.MonoAOTCompiler.Task": {
+    "Microsoft.NET.Runtime.MonoTargets.Sdk": {
       "kind": "Sdk",
       "version": "${PackageVersion}"
     },
       "version": "${PackageVersion}"
     },
   }
-}
\ No newline at end of file
+}
index 4176041..c93e501 100644 (file)
 
     <Import Condition="'$(RunAOTCompilation)' == 'true'" Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoAOTCompiler.Task" />
 
-    <Import Condition="'$(TargetPlatformIdentifier)' == 'android'" Project="Sdk.props" Sdk="Microsoft.NET.Runtime.RuntimeConfigParser.Task" />
+    <ImportGroup Condition="'$(TargetPlatformIdentifier)' == 'android'">
+        <Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
+        <Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
+    </ImportGroup>
     <ImportGroup Condition="'$(TargetPlatformIdentifier)' == 'android' and '$(RunAOTCompilation)' == 'true'">
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.android-x86" />
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.android-x64" />
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.android-arm64" />
     </ImportGroup>
 
-    <ImportGroup Condition="'$(TargetPlatformIdentifier)' == 'ios'">
-        <Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.RuntimeConfigParser.Task" />
-    </ImportGroup>
     <ImportGroup Condition="'$(TargetPlatformIdentifier)' == 'ios' and $([MSBuild]::IsOSPlatform('osx'))">
+        <Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.ios-arm" />
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.ios-arm64" />
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator" />
+        <Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
     </ImportGroup>
 
-    <ImportGroup Condition="'$(TargetPlatformIdentifier)' == 'maccatalyst'">
-        <Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.RuntimeConfigParser.Task" />
-    </ImportGroup>
     <ImportGroup Condition="'$(TargetPlatformIdentifier)' == 'maccatalyst' and $([MSBuild]::IsOSPlatform('osx'))">
+        <Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.maccatalyst" />
+        <Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
     </ImportGroup>
 
-    <ImportGroup Condition="'$(TargetPlatformIdentifier)' == 'tvos'">
-        <Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.RuntimeConfigParser.Task" />
-    </ImportGroup>
     <ImportGroup Condition="'$(TargetPlatformIdentifier)' == 'tvos' and $([MSBuild]::IsOSPlatform('osx'))">
+        <Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.tvos-arm64" />
+        <Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
+    </ImportGroup>
+
+    <ImportGroup Condition="'$(TargetPlatformIdentifier)' == 'tvossimulator'">
+        <Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.tvossimulator" />
+        <Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
     </ImportGroup>
 
     <ImportGroup Condition="'$(RuntimeIdentifier)' == 'browser-wasm' and '$(UsingBrowserRuntimeWorkload)' == 'true'">
+        <Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
         <Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.WebAssembly.Sdk" />
         <Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.browser-wasm" />
+        <Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk" />
     </ImportGroup>
+
 </Project>
index 9403ca9..a56c63f 100644 (file)
@@ -16,6 +16,9 @@
   <ItemGroup Condition="'$(TargetsMobile)' == 'true'">
     <ProjectReference Include="Microsoft.NET.Workload.Mono.Toolchain.Manifest\Microsoft.NET.Workload.Mono.Toolchain.Manifest.pkgproj" />
     <ProjectReference Include="Microsoft.NET.Runtime.MonoAOTCompiler.Task\Microsoft.NET.Runtime.MonoAOTCompiler.Task.pkgproj" />
-    <ProjectReference Include="Microsoft.NET.Runtime.RuntimeConfigParser.Task\Microsoft.NET.Runtime.RuntimeConfigParser.Task.pkgproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="Microsoft.NET.Runtime.MonoTargets.Sdk\Microsoft.NET.Runtime.MonoTargets.Sdk.pkgproj" />
   </ItemGroup>
 </Project>
diff --git a/src/tasks/JsonToItemsTaskFactory/JsonToItemsTaskFactory.cs b/src/tasks/JsonToItemsTaskFactory/JsonToItemsTaskFactory.cs
new file mode 100644 (file)
index 0000000..2f7d1dc
--- /dev/null
@@ -0,0 +1,398 @@
+// 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;
+
+#if NET472
+namespace System.Diagnostics.CodeAnalysis {
+    [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
+
+    public class NotNullWhenAttribute : Attribute {
+        public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
+        public bool ReturnValue { get; }
+    }
+}
+#endif
+
+namespace JsonToItemsTaskFactory
+{
+
+    /// <summary>Reads a json input blob and populates some output items</summary>
+    ///
+    /// <example>JSON should follow this structure - the toplevel "properties" and "items" keys are exact, other keys are arbitrary.
+    /// <code>
+    /// {
+    ///    "properties" : {
+    ///      "propName1": "value1",
+    ///      "propName2": "value"
+    ///    },
+    ///    "items" : {
+    ///      "itemName1": [ "stringValue", { "identity": "anotherValue", "metadataKey": "metadataValue", ... }, "thirdValue" ],
+    ///      "itemName2": [ ... ]
+    /// }
+    /// </code>
+    ///
+    /// A task can be declared by
+    ///
+    /// <code>
+    /// <UsingTask AssemblyFile="..." TaskName="MyJsonReader" TaskFactory="Microsoft.DotNet.Runtime.Tasks.JsonToItemsTaskFactory">
+    ///   <ParameterGroup>
+    ///     <PropName1 ParameterType="System.String" Required="False" Output="True" />
+    ///     <ItemName1 ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="False" Output="True" />
+    ///   <ParameterGroup>
+    /// </UsingTask>
+    /// </code>
+    ///
+    /// And then used in a target.  The `JsonFilePath' attribute is used to specify the json file to read.
+    ///
+    /// <code>
+    /// <Target Name="UseMyReader">
+    ///   <MyJsonReader JsonFilePath="foo.json">
+    ///     <Output TaskParameter="PropName1" PropertyName="MyParsedProperty" />
+    ///     <Output TaskParameter="ItemName1" ItemName="MyParsedItems" />
+    ///   </MyJsonReader>
+    ///   <Message Priority="High" Text=" Got property $(MyParsedProperty) and items @(MyParsedItems)" />
+    /// </Target>
+    /// </code>
+    /// </example>
+    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<string, TaskPropertyInfo> 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<string, TaskPropertyInfo> 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 (!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
+                {
+                    if (file != null)
+                        file.Dispose();
+                }
+            }
+
+            public async Task<JsonModelRoot?> GetJsonAsync(string jsonFilePath, FileStream file)
+            {
+                JsonModelRoot? json = null;
+                try
+                {
+                    json = await JsonSerializer.DeserializeAsync<JsonModelRoot>(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<string, string>? Properties {get; set;}
+            public Dictionary<string, JsonModelItem[]>? Items {get; set;}
+
+            public JsonModelRoot() {}
+        }
+
+        [JsonConverter(typeof(JsonModelItemConverter))]
+        public class JsonModelItem
+        {
+            public string Identity {get;}
+            // n.b. will  be deserialized case insensitive
+            public Dictionary<string, string>? Metadata {get;}
+
+            public JsonModelItem(string identity, Dictionary<string, string>? metadata)
+            {
+                Identity = identity;
+                Metadata = metadata;
+            }
+        }
+
+        public class CaseInsensitiveDictionaryConverter : JsonConverter<Dictionary<string, string>>
+        {
+            public override Dictionary<string, string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+            {
+                var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(ref reader, options);
+                if (dict == null)
+                    return null!;
+                return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
+            }
+            public override void Write(Utf8JsonWriter writer, Dictionary<string, string>? value, JsonSerializerOptions options) =>
+                JsonSerializer.Serialize(writer, value, options);
+        }
+        public  class JsonModelItemConverter : JsonConverter<JsonModelItem>
+        {
+            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<Dictionary<string, string>>(ref reader, options);
+                        if (dict == null)
+                            return null!;
+                        var idict = new Dictionary<string, string> (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/JsonToItemsTaskFactory/JsonToItemsTaskFactory.csproj b/src/tasks/JsonToItemsTaskFactory/JsonToItemsTaskFactory.csproj
new file mode 100644 (file)
index 0000000..d6df214
--- /dev/null
@@ -0,0 +1,32 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFrameworks>$(TargetFrameworkForNETCoreTasks);$(TargetFrameworkForNETFrameworkTasks)</TargetFrameworks>
+    <OutputType>Library</OutputType>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
+    <Nullable>enable</Nullable>
+    <!-- Ignore nullable warnings on net4* -->
+    <!-- NoWarn Condition="$(TargetFramework.StartsWith('net4'))">$(NoWarn),CS8604,CS8602</NoWarn -->
+  </PropertyGroup>
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Build" Version="$(RefOnlyMicrosoftBuildVersion)" />
+    <PackageReference Include="Microsoft.Build.Framework" Version="$(RefOnlyMicrosoftBuildFrameworkVersion)" />
+    <PackageReference Include="Microsoft.Build.Tasks.Core" Version="$(RefOnlyMicrosoftBuildTasksCoreVersion)" />
+    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="$(RefOnlyMicrosoftBuildUtilitiesCoreVersion)" />
+    <PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataVersion)" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="JsonToItemsTaskFactory.cs" />
+  </ItemGroup>
+
+  <!-- GetFilesToPackage assists to place `JsonToItemsTaskFactory.dll` in a NuGet package in Microsoft.NET.Runtime.MonoTargets.Sdk.pkgproj for external use -->
+  <Target Name="GetFilesToPackage" Returns="@(FilesToPackage)">
+    <ItemGroup>
+      <_PublishFramework Remove="@(_PublishFramework)" />
+      <_PublishFramework Include="$(TargetFrameworks)" />
+
+      <FilesToPackage Include="$(OutputPath)%(_PublishFramework.Identity)\$(AssemblyName).dll" TargetPath="tasks\%(_PublishFramework.Identity)" />
+    </ItemGroup>
+  </Target>
+
+</Project>
diff --git a/src/tasks/JsonToItemsTaskFactory/README.md b/src/tasks/JsonToItemsTaskFactory/README.md
new file mode 100644 (file)
index 0000000..619991a
--- /dev/null
@@ -0,0 +1,86 @@
+# 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
+<UsingTask TaskName="MyJsonReader" AssemblyFile="..\JsonToItemsTaskFactory\bin\Debug\net6.0\JsonToItemsTaskFactory.dll"
+        TaskFactory="JsonToItemsTaskFactory.JsonToItemsTaskFactory">
+    <ParameterGroup>
+        <X1 ParameterType="System.String" Required="false" Output="true" />
+        <FunFiles ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
+        <FilesWithMeta ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" Output="true" />
+    </ParameterGroup>
+</UsingTask>
+```
+
+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
+<Target Name="RunMe">
+    <MyJsonReader JsonFilePath="$(MSBuildThisFileDirectory)\example.jsonc">
+        <Output TaskParameter="X1" PropertyName="FromJsonX1" />
+        <Output TaskParameter="FunFiles" ItemName="FromJsonFunFiles" />
+        <Output TaskParameter="FilesWithMeta" ItemName="FromJsonWithMeta" />
+    </MyJsonReader> 
+    <Message Importance="High" Text="X1 = $(FromJsonX1)" />
+    <Message Importance="High" Text="FunFiles = @(FromJsonFunFiles)" />
+    <ItemGroup>
+        <FromJsonWithMetaWithTP Include="@(FromJsonWithMeta->HasMetadata('TargetPath'))" />
+        <FromJsonWithMetaWithoutTP Include="@(FromJsonWithMeta)" Exclude="@(FromJsonWithMetaWithTP)" />
+    </ItemGroup>
+    <Message Importance="High" Text="FilesWithMeta = @(FromJsonWithMetaWithTP)  TargetPath='%(TargetPath)'"/>
+    <Message Importance="High" Text="FilesWithMeta = @(FromJsonWithMetaWithoutTP)  (No TargetPath)" />
+</Target>
+```
+
+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
+```
+