Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 22 additions & 2 deletions dotnet/targets/Xamarin.Shared.Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.InstallNameTool" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.LinkNativeCode" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.MergeAppBundles" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.MergeRuntimeConfigFiles" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.MobileILStrip" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.MacDevMessage" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.PostTrimmingProcessing" AssemblyFile="$(_TaskAssemblyName)" />
Expand Down Expand Up @@ -1593,10 +1594,13 @@
<!-- App bundle creation tasks -->

<Target Name="_CreateRuntimeConfiguration"
Inputs="$(ProjectRuntimeConfigFilePath)"
Inputs="$(ProjectRuntimeConfigFilePath);$(ProjectRuntimeConfigDevFilePath)"
Outputs="$(_ParsedRuntimeConfigFilePath)"
DependsOnTargets="GenerateBuildRuntimeConfigurationFiles;_ComputePublishLocation"
>
<PropertyGroup>
<_MergedRuntimeConfigFilePath>$(DeviceSpecificIntermediateOutputPath)runtimeconfig.merged.json</_MergedRuntimeConfigFilePath>
</PropertyGroup>
<ItemGroup>
<!-- List all the properties passed to xamarin_bridge_vm_initialize in xamarin_vm_initialize (in runtime.m) -->
<_RuntimeConfigReservedProperties Include="APP_PATHS" />
Expand All @@ -1608,9 +1612,25 @@
<_RuntimeConfigReservedProperties Include="SYSTEM_CORELIB_DIRECTORY" />
<_RuntimeConfigReservedProperties Include="STARTUP_HOOKS" />
</ItemGroup>
<RuntimeConfigParserTask

<!-- Merge the runtimeconfig.dev.json (Debug-only switches such as Hot Reload) on top of the main
runtimeconfig.json, because we hand the runtime configuration directly to the runtime and thus
bypass hostfxr, which is what would normally merge these files. -->
<MergeRuntimeConfigFiles
Condition="'$(GenerateRuntimeConfigurationFiles)' == 'true'"
RuntimeConfigFile="$(ProjectRuntimeConfigFilePath)"
RuntimeConfigDevFile="$(ProjectRuntimeConfigDevFilePath)"
OutputFile="$(_MergedRuntimeConfigFilePath)"
>
</MergeRuntimeConfigFiles>

<ItemGroup Condition="'$(GenerateRuntimeConfigurationFiles)' == 'true'">
<FileWrites Include="$(_MergedRuntimeConfigFilePath)" />
</ItemGroup>

<RuntimeConfigParserTask
Condition="'$(GenerateRuntimeConfigurationFiles)' == 'true'"
RuntimeConfigFile="$(_MergedRuntimeConfigFilePath)"
OutputFile="$(_ParsedRuntimeConfigFilePath)"
RuntimeConfigReservedProperties="@(_RuntimeConfigReservedProperties)"
>
Expand Down
5 changes: 5 additions & 0 deletions msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1705,4 +1705,9 @@
<value>Codesign failed with 'errSecInternalComponent'. This usually means the keychain is locked, which is common when building over SSH. Unlock the keychain first, for example by running 'security unlock-keychain ~/Library/Keychains/login.keychain-db'.</value>
<comment>Shown when codesign fails with the 'errSecInternalComponent' error, which typically indicates a locked keychain.</comment>
</data>
<data name="E7185" xml:space="preserve">
<value>The runtime configuration file '{0}' is not a valid JSON object.</value>
<comment>Shown when the main runtime configuration file (*.runtimeconfig.json) does not contain a valid JSON object at its root.
{0} - The path to the runtime configuration file.</comment>
</data>
</root>
72 changes: 72 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/MergeRuntimeConfigFiles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;

using Microsoft.Build.Framework;

#nullable enable

namespace Xamarin.MacDev.Tasks {
// This task merges the 'runtimeOptions.configProperties' from a '*.runtimeconfig.dev.json' file on top of
// the ones from the main '*.runtimeconfig.json' file, writing the merged result to an output file.
//
// We do this because we hand the runtime configuration directly to the runtime (bypassing hostfxr), and
// hostfxr is what would normally merge the dev file (which contains Debug-only switches such as Hot Reload)
// into the main runtime configuration. The dev values win, matching hostfxr's behavior.
public class MergeRuntimeConfigFiles : XamarinTask {
[Required]
public string RuntimeConfigFile { get; set; } = "";

public string? RuntimeConfigDevFile { get; set; }

[Required]
public string OutputFile { get; set; } = "";

static readonly JsonDocumentOptions documentOptions = new JsonDocumentOptions {
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip,
};

public override bool Execute ()
{
var mainNode = JsonNode.Parse (File.ReadAllText (RuntimeConfigFile), documentOptions: documentOptions);
if (mainNode is not JsonObject mainObject) {
Log.LogError (MSBStrings.E7185 /* The runtime configuration file '{0}' is not a valid JSON object. */, RuntimeConfigFile);
return false;
}

if (!string.IsNullOrEmpty (RuntimeConfigDevFile) && File.Exists (RuntimeConfigDevFile)) {
var devNode = JsonNode.Parse (File.ReadAllText (RuntimeConfigDevFile!), documentOptions: documentOptions);
var devConfigProperties = (devNode as JsonObject)? ["runtimeOptions"]? ["configProperties"] as JsonObject;
if (devConfigProperties is not null && devConfigProperties.Count > 0) {
var runtimeOptions = mainObject ["runtimeOptions"] as JsonObject;
if (runtimeOptions is null) {
runtimeOptions = new JsonObject ();
mainObject ["runtimeOptions"] = runtimeOptions;
}

var configProperties = runtimeOptions ["configProperties"] as JsonObject;
if (configProperties is null) {
configProperties = new JsonObject ();
runtimeOptions ["configProperties"] = configProperties;
}

foreach (var property in devConfigProperties) {
configProperties [property.Key] = property.Value is null ? null : JsonNode.Parse (property.Value.ToJsonString ());
}
}
}
Comment thread
rolfbjarne marked this conversation as resolved.
Outdated

var outputDirectory = Path.GetDirectoryName (OutputFile);
if (!string.IsNullOrEmpty (outputDirectory))
Directory.CreateDirectory (outputDirectory!);

File.WriteAllText (OutputFile, mainObject.ToJsonString (new JsonSerializerOptions { WriteIndented = true }));
Comment thread
rolfbjarne marked this conversation as resolved.
Outdated

return !Log.HasLoggedErrors;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO;

using NUnit.Framework;

#nullable enable

namespace Xamarin.MacDev.Tasks {

// Note: we can't use System.Text.Json types here to inspect the output, because the
// Xamarin.MacDev.Tasks assembly under test is ILMerged and also exposes System.Text.Json,
// which causes ambiguous type references. We assert on the raw JSON text instead.
[TestFixture]
public class MergeRuntimeConfigFilesTaskTest : TestBase {

string RunMerge (string mainJson, string? devJson)
{
var tmp = Cache.CreateTemporaryDirectory ();
var mainFile = Path.Combine (tmp, "app.runtimeconfig.json");
File.WriteAllText (mainFile, mainJson);

string? devFile = null;
if (devJson is not null) {
devFile = Path.Combine (tmp, "app.runtimeconfig.dev.json");
File.WriteAllText (devFile, devJson);
}

var outputFile = Path.Combine (tmp, "obj", "runtimeconfig.merged.json");

var task = CreateTask<MergeRuntimeConfigFiles> ();
task.RuntimeConfigFile = mainFile;
task.RuntimeConfigDevFile = devFile;
task.OutputFile = outputFile;

ExecuteTask (task);

Assert.That (outputFile, Does.Exist, "output file created");
return File.ReadAllText (outputFile);
}

[Test]
public void MergesDevPropertiesOntoMain ()
{
var mainJson = @"{
""runtimeOptions"": {
""tfm"": ""net10.0"",
""configProperties"": {
""OnlyInMain"": true,
""InBoth"": ""main-value""
}
}
}";
var devJson = @"{
""runtimeOptions"": {
""configProperties"": {
""OnlyInDev"": ""dev-only"",
""InBoth"": ""dev-value""
}
}
}";

var merged = RunMerge (mainJson, devJson);

// (a) properties only in main are preserved
Assert.That (merged, Does.Contain ("\"OnlyInMain\": true"), "OnlyInMain preserved");
// (b) properties only in dev are added
Assert.That (merged, Does.Contain ("\"OnlyInDev\": \"dev-only\""), "OnlyInDev added");
// (c) properties in both are taken from dev (dev wins)
Assert.That (merged, Does.Contain ("\"InBoth\": \"dev-value\""), "dev wins for InBoth");
Assert.That (merged, Does.Not.Contain ("main-value"), "main value for InBoth is gone");
}

[Test]
public void NoDevFile ()
{
var mainJson = @"{
""runtimeOptions"": {
""configProperties"": {
""OnlyInMain"": true
}
}
}";

var merged = RunMerge (mainJson, null);

Assert.That (merged, Does.Contain ("\"OnlyInMain\": true"), "OnlyInMain preserved");
}

[Test]
public void DevFileWithoutConfigProperties ()
{
var mainJson = @"{
""runtimeOptions"": {
""configProperties"": {
""OnlyInMain"": true
}
}
}";
var devJson = @"{
""runtimeOptions"": {
""additionalProbingPaths"": [ ""/some/path"" ]
}
}";

var merged = RunMerge (mainJson, devJson);

Assert.That (merged, Does.Contain ("\"OnlyInMain\": true"), "OnlyInMain preserved");
// The dev file had no configProperties, so nothing from it should have leaked in.
Assert.That (merged, Does.Not.Contain ("additionalProbingPaths"), "dev-only non-configProperties content is not merged");
}
}
}
Loading