Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add msbuild task to generate binary runtimeconfig format #49544

Merged
merged 10 commits into from
Mar 17, 2021
2 changes: 2 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
<WasmAppBuilderDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'WasmAppBuilder', 'Debug', '$(NetCoreAppToolCurrent)', 'publish'))</WasmAppBuilderDir>
<WasmBuildTasksDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'WasmBuildTasks', 'Debug', '$(NetCoreAppToolCurrent)', 'publish'))</WasmBuildTasksDir>
<MonoAOTCompilerDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'MonoAOTCompiler', 'Debug', '$(NetCoreAppToolCurrent)'))</MonoAOTCompilerDir>
<RuntimeConfigParserDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'RuntimeConfigParser', 'Debug', '$(NetCoreAppToolCurrent)', 'publish'))</RuntimeConfigParserDir>

<InstallerTasksAssemblyPath Condition="'$(MSBuildRuntimeType)' == 'Core'">$([MSBuild]::NormalizePath('$(ArtifactsBinDir)', 'installer.tasks', 'Debug', '$(NetCoreAppToolCurrent)', 'installer.tasks.dll'))</InstallerTasksAssemblyPath>
<InstallerTasksAssemblyPath Condition="'$(MSBuildRuntimeType)' != 'Core'">$([MSBuild]::NormalizePath('$(ArtifactsBinDir)', 'installer.tasks', 'Debug', 'net461', 'installer.tasks.dll'))</InstallerTasksAssemblyPath>
Expand All @@ -78,6 +79,7 @@
<WasmAppBuilderTasksAssemblyPath>$([MSBuild]::NormalizePath('$(WasmAppBuilderDir)', 'WasmAppBuilder.dll'))</WasmAppBuilderTasksAssemblyPath>
<WasmBuildTasksAssemblyPath>$([MSBuild]::NormalizePath('$(WasmBuildTasksDir)', 'WasmBuildTasks.dll'))</WasmBuildTasksAssemblyPath>
<MonoAOTCompilerTasksAssemblyPath>$([MSBuild]::NormalizePath('$(MonoAOTCompilerDir)', 'MonoAOTCompiler.dll'))</MonoAOTCompilerTasksAssemblyPath>
<RuntimeConfigParserTasksAssemblyPath>$([MSBuild]::NormalizePath('$(RuntimeConfigParserDir)', 'RuntimeConfigParser.dll'))</RuntimeConfigParserTasksAssemblyPath>
</PropertyGroup>

<PropertyGroup Label="CalculateConfiguration">
Expand Down
108 changes: 108 additions & 0 deletions src/tasks/RuntimeConfigParser/RuntimeConfigParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Reflection.Metadata;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class RuntimeConfigParserTask : Task
{
/// <summary>
/// The path to runtimeconfig.json file.
/// </summary>
[Required]
public string RuntimeConfigFile { get; set; } = "";

/// <summary>
/// The path to the output binary file.
/// </summary>
[Required]
public string OutputFile { get; set; } = "";

/// <summary>
/// List of reserved properties.
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public ITaskItem[] ReservedProperties { get; set; } = Array.Empty<ITaskItem>();

public override bool Execute()
{
if (string.IsNullOrEmpty(RuntimeConfigFile))
{
throw new ArgumentException($"'{nameof(RuntimeConfigFile)}' is required.", nameof(RuntimeConfigFile));
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
}

if (string.IsNullOrEmpty(OutputFile))
{
throw new ArgumentException($"'{nameof(OutputFile)}' is required.", nameof(OutputFile));
}

Dictionary<string, string> configProperties = ConvertInputToDictionary(RuntimeConfigFile);

if (ReservedProperties.Length != 0)
{
checkDuplicateProperties(configProperties, ReservedProperties);
}

var blobBuilder = new BlobBuilder();
ConvertDictionaryToBlob(configProperties, blobBuilder);

using var stream = File.OpenWrite(OutputFile);
blobBuilder.WriteContentTo(stream);

return true;
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
}

/// Reads a json file from the given path and extracts the "configProperties" key (assumed to be a string to string dictionary)
private Dictionary<string, string> ConvertInputToDictionary(string inputFilePath)
{
var options = new JsonSerializerOptions {
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
};

var jsonString = File.ReadAllText(inputFilePath);
var parsedJson = JsonSerializer.Deserialize<Root>(jsonString, options);

return parsedJson!.ConfigProperties;
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
}

/// Just write the dictionary out to a blob as a count followed by
/// a length-prefixed UTF8 encoding of each key and value
private void ConvertDictionaryToBlob(IReadOnlyDictionary<string, string> properties, BlobBuilder b)
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
{
int count = properties.Count;

b.WriteCompressedInteger(count);
foreach (var kvp in properties)
{
b.WriteSerializedString(kvp.Key);
b.WriteSerializedString(kvp.Value);
}
}

private void checkDuplicateProperties(IReadOnlyDictionary<string, string> properties, ITaskItem[] keys)
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
{
foreach (var key in keys)
{
if (properties.ContainsKey(key.ItemSpec))
{
throw new ArgumentException($"Property '{key}' can't be set by the user!");
}
}
}
}

public class Root
{
// the configProperties key
[JsonPropertyName ("configProperties")]
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
public Dictionary<string, string> ConfigProperties {get; set;} = new Dictionary<string, string> ();
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
// everything other than configProperties
[JsonExtensionData]
public Dictionary<string, object> ExtensionData {get; set;} = new Dictionary<string, object> ();
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
}
27 changes: 27 additions & 0 deletions src/tasks/RuntimeConfigParser/RuntimeConfigParser.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(NetCoreAppToolCurrent)</TargetFramework>
<OutputType>Library</OutputType>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn),CA1050</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="5.0.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="RuntimeConfigParser.cs" />
</ItemGroup>

<Target Name="PublishBuilder"
AfterTargets="Build"
DependsOnTargets="Publish" />

<Target Name="GetFilesToPackage" />

</Project>