Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

#nullable disable

using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using NuGet.Frameworks;
using NuGet.ProjectModel;

Expand Down Expand Up @@ -281,13 +281,11 @@ private void AddUserRuntimeOptions(RuntimeOptions runtimeOptions)
return;
}

JObject runtimeOptionsFromProject;
using (JsonTextReader reader = new(File.OpenText(userConfigPath)))
{
runtimeOptionsFromProject = JObject.Load(reader);
}
JsonObject runtimeOptionsFromProject = (JsonObject)JsonNode.Parse(
File.ReadAllText(userConfigPath),
documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true });

foreach (var runtimeOption in runtimeOptionsFromProject)
foreach (KeyValuePair<string, JsonNode> runtimeOption in runtimeOptionsFromProject)
{
runtimeOptions.RawOptions.Add(runtimeOption.Key, runtimeOption.Value);
}
Expand All @@ -300,45 +298,41 @@ private void AddHostConfigurationOptions(RuntimeOptions runtimeOptions)
return;
}

JObject configProperties = GetConfigProperties(runtimeOptions);
JsonObject configProperties = GetConfigProperties(runtimeOptions);

foreach (var hostConfigurationOption in HostConfigurationOptions)
{
configProperties[hostConfigurationOption.ItemSpec] = GetConfigPropertyValue(hostConfigurationOption);
}
}

private static JObject GetConfigProperties(RuntimeOptions runtimeOptions)
private static JsonObject GetConfigProperties(RuntimeOptions runtimeOptions)
{
JToken configProperties;
if (!runtimeOptions.RawOptions.TryGetValue("configProperties", out configProperties)
|| configProperties == null
|| configProperties.Type != JTokenType.Object)
if (!runtimeOptions.RawOptions.TryGetValue("configProperties", out JsonNode configProperties)
|| configProperties is not JsonObject)
{
configProperties = new JObject();
configProperties = new JsonObject();
runtimeOptions.RawOptions["configProperties"] = configProperties;
}

return (JObject)configProperties;
return (JsonObject)configProperties;
}

private static JToken GetConfigPropertyValue(ITaskItem hostConfigurationOption)
private static JsonNode GetConfigPropertyValue(ITaskItem hostConfigurationOption)
{
string valueString = hostConfigurationOption.GetMetadata("Value");

bool boolValue;
if (bool.TryParse(valueString, out boolValue))
if (bool.TryParse(valueString, out bool boolValue))
{
return new JValue(boolValue);
return JsonValue.Create(boolValue);
}

int intValue;
if (int.TryParse(valueString, out intValue))
if (int.TryParse(valueString, out int intValue))
{
return new JValue(intValue);
return JsonValue.Create(intValue);
}

return new JValue(valueString);
return JsonValue.Create(valueString);
}

private void WriteDevRuntimeConfig(IList<LockFileItem> packageFolders)
Expand Down Expand Up @@ -391,19 +385,115 @@ private static string EnsureNoTrailingDirectorySeparator(string path)
return path;
}

private static void WriteToJsonFile(string fileName, object value)
private static void WriteToJsonFile(string fileName, RuntimeConfig value)
{
JsonSerializer serializer = new()
// Build the document explicitly with the System.Text.Json node API instead of a
// reflection-based serializer, which is not trim/AOT safe (IL2026/IL3050). The writer
// is configured to reproduce the previous output exactly: 2-space indentation,
// environment newlines, and relaxed escaping (so characters such as '+' in paths are
// written verbatim rather than as \uXXXX escapes).
JsonObject json = new()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Formatting = Formatting.Indented,
DefaultValueHandling = DefaultValueHandling.Ignore
["runtimeOptions"] = SerializeRuntimeOptions(value.RuntimeOptions)
};

using (JsonTextWriter writer = new(new StreamWriter(File.Create(fileName))))
JsonWriterOptions options = new()
{
Indented = true,
NewLine = Environment.NewLine,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

using (FileStream stream = File.Create(fileName))
using (Utf8JsonWriter writer = new(stream, options))
{
json.WriteTo(writer);
}
}

private static JsonObject SerializeRuntimeOptions(RuntimeOptions runtimeOptions)
{
// Declared properties are emitted in declaration order. Null values are omitted.
JsonObject json = new();

if (runtimeOptions.Tfm != null)
{
json["tfm"] = runtimeOptions.Tfm;
}

if (runtimeOptions.RollForward != null)
{
json["rollForward"] = runtimeOptions.RollForward;
}

if (runtimeOptions.Framework != null)
{
json["framework"] = SerializeFramework(runtimeOptions.Framework);
}

if (runtimeOptions.Frameworks != null)
{
json["frameworks"] = SerializeFrameworks(runtimeOptions.Frameworks);
}

if (runtimeOptions.IncludedFrameworks != null)
{
json["includedFrameworks"] = SerializeFrameworks(runtimeOptions.IncludedFrameworks);
}

if (runtimeOptions.AdditionalProbingPaths != null)
{
JsonArray probingPaths = new();
foreach (string probingPath in runtimeOptions.AdditionalProbingPaths)
{
// Cast to JsonNode so the non-generic Add overload is used; the generic
// JsonArray.Add<T> is annotated as trim/AOT unsafe (IL2026/IL3050).
probingPaths.Add((JsonNode)probingPath);
}

json["additionalProbingPaths"] = probingPaths;
}

// RawOptions is the extension-data bag; its entries are written as direct children of
// runtimeOptions, after the declared properties. Clone each value so it can be
// re-parented into this document regardless of where it originated.
foreach (KeyValuePair<string, JsonNode> rawOption in runtimeOptions.RawOptions)
{
serializer.Serialize(writer, value);
json[rawOption.Key] = rawOption.Value?.DeepClone();
}

return json;
}

private static JsonArray SerializeFrameworks(List<RuntimeConfigFramework> frameworks)
{
JsonArray array = new();

foreach (RuntimeConfigFramework framework in frameworks)
{
// Cast to JsonNode to use the non-generic Add overload (the generic
// JsonArray.Add<T> is annotated as trim/AOT unsafe).
array.Add((JsonNode)SerializeFramework(framework));
}

return array;
}

private static JsonObject SerializeFramework(RuntimeConfigFramework framework)
{
JsonObject json = new();

if (framework.Name != null)
{
json["name"] = framework.Name;
}

if (framework.Version != null)
{
json["version"] = framework.Version;
}

return json;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<Description>The MSBuild targets and properties for building .NET Core projects.</Description>
<OutputType>Library</OutputType>
<TargetFrameworks>$(SdkTargetFramework);net472</TargetFrameworks>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
</PropertyGroup>

<PropertyGroup>
Expand Down
7 changes: 2 additions & 5 deletions src/Tasks/Microsoft.NET.Build.Tasks/RuntimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@

#nullable disable

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text.Json.Nodes;

namespace Microsoft.NET.Build.Tasks
{
internal class RuntimeOptions
{
public string Tfm { get; set; }

[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string RollForward { get; set; }

public RuntimeConfigFramework Framework { get; set; }
Expand All @@ -23,8 +21,7 @@ internal class RuntimeOptions

public List<string> AdditionalProbingPaths { get; set; }

[JsonExtensionData]
public IDictionary<string, JToken> RawOptions { get; } = new Dictionary<string, JToken>();
public IDictionary<string, JsonNode> RawOptions { get; } = new Dictionary<string, JsonNode>();

public RuntimeOptions()
{
Expand Down
Loading
Loading