Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
23 changes: 23 additions & 0 deletions docs/building-apps/build-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,29 @@ See also:

An item group that contains atlas textures.

## AppManifestEntry

An item group that contains entries to add to the app manifest (Info.plist).

These entries are processed last and override entries from the main app
manifest, partial app manifests, and generated values.

The following types are supported:

```xml
<ItemGroup>
<AppManifestEntry Include="BooleanKey" Type="Boolean" Value="true" />
<AppManifestEntry Include="StringKey" Type="String" Value="stringvalue" />
<AppManifestEntry Include="StringArrayKey" Type="StringArray" Value="a;b" />
<AppManifestEntry Include="StringArrayKeyWithCustomSeparator" Type="StringArray" Value="a|b" ArraySeparator="|" />
<AppManifestEntry Include="KeyToRemove" Type="Remove" />
</ItemGroup>
```

Boolean values may be `true` or `false` (case-insensitive). String arrays use a
semicolon as the default separator, which can be changed with the
`ArraySeparator` metadata.

## BGenReferencePath

The list of assembly references to pass to the `bgen` tool (binding generator).
Expand Down
20 changes: 20 additions & 0 deletions msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,26 @@
{1} - The assembly path.
{2} - The computed extraction path.
{3} - The intended output directory.</comment>
</data>
<data name="E7184" xml:space="preserve">
<value>Invalid value '{0}' for the app manifest entry '{1}' of type '{2}' specified in the AppManifestEntry item group. Expected no value at all.</value>
<comment>Shown when an AppManifestEntry item of type Remove has a value.
{0} - The invalid value.
{1} - The app manifest entry name.
{2} - The app manifest entry type.</comment>
</data>
<data name="E7185" xml:space="preserve">
<value>Invalid value '{0}' for the app manifest entry '{1}' of type '{2}' specified in the AppManifestEntry item group. Expected 'true' or 'false'.</value>
<comment>Shown when an AppManifestEntry item of type Boolean has an invalid value.
{0} - The invalid value.
{1} - The app manifest entry name.
{2} - The app manifest entry type.</comment>
</data>
<data name="E7186" xml:space="preserve">
<value>Unknown type '{0}' for the app manifest entry '{1}' specified in the AppManifestEntry item group. Expected 'Remove', 'Boolean', 'String', or 'StringArray'.</value>
<comment>Shown when an AppManifestEntry item has an unknown type.
{0} - The unknown type.
{1} - The app manifest entry name.</comment>
</data>
<data name="E0192" xml:space="preserve">
<value>The PrepareAssemblies task failed without reporting a specific error. Please rebuild with increased verbosity for more details.</value>
Expand Down
43 changes: 43 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/CompileAppManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public class CompileAppManifest : XamarinTask, IHasProjectDir, IHasResourcePrefi
// This must be an ITaskItem to copy the file to Windows for remote builds.
public ITaskItem? AppManifest { get; set; }

public ITaskItem [] AppManifestEntries { get; set; } = [];

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

Expand Down Expand Up @@ -157,6 +159,8 @@ public override bool Execute ()
// Merge with any partial plists...
MergePartialPlistTemplates (plist);

AddAppManifestEntries (plist);

Validation (plist);

// write the resulting app manifest
Expand All @@ -180,6 +184,45 @@ void AddXamarinVersionNumber (PDictionary plist)
plist.Add (name, dict);
}

void AddAppManifestEntries (PDictionary plist)
Comment thread
rolfbjarne marked this conversation as resolved.
{
foreach (var item in AppManifestEntries) {
var key = item.ItemSpec;
var type = item.GetMetadata ("Type");
var value = item.GetMetadata ("Value");

switch (type.ToLowerInvariant ()) {
case "remove":
if (!string.IsNullOrEmpty (value))
Log.LogError (MSBStrings.E7184, /* Invalid value '{0}' for the app manifest entry '{1}' of type '{2}' specified in the AppManifestEntry item group. Expected no value at all. */ value, key, type);
plist.Remove (key);
break;
case "boolean":
if (!bool.TryParse (value, out var booleanValue)) {
Log.LogError (MSBStrings.E7185, /* Invalid value '{0}' for the app manifest entry '{1}' of type '{2}' specified in the AppManifestEntry item group. Expected 'true' or 'false'. */ value, key, type);
continue;
}
plist [key] = new PBoolean (booleanValue);
break;
case "string":
plist [key] = new PString (value);
break;
case "stringarray":
var arraySeparator = item.GetMetadata ("ArraySeparator");
if (string.IsNullOrEmpty (arraySeparator))
arraySeparator = ";";
var array = new PArray ();
foreach (var element in value.Split (new [] { arraySeparator }, StringSplitOptions.None))
array.Add (new PString (element));
plist [key] = array;
break;
default:
Log.LogError (MSBStrings.E7186, /* Unknown type '{0}' for the app manifest entry '{1}' specified in the AppManifestEntry item group. Expected 'Remove', 'Boolean', 'String', or 'StringArray'. */ type, key);
break;
}
}
}

void RegisterFonts (PDictionary plist)
{
if (FontFilesToRegister is null || FontFilesToRegister.Length == 0)
Expand Down
13 changes: 8 additions & 5 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/ComputeHashForItems.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
Expand Down Expand Up @@ -40,14 +39,18 @@ public override bool Execute ()

using var sha = CreateHashAlgorithm ();

var buffer = new List<byte> ();
for (var i = 0; i < Input.Length; i++) {
var input = Input [i];
buffer.Clear ();
using var buffer = new MemoryStream ();
using var writer = new BinaryWriter (buffer, Encoding.UTF8, true);
foreach (var im in InputMetadata) {
Comment thread
rolfbjarne marked this conversation as resolved.
buffer.AddRange (Encoding.UTF8.GetBytes (input.GetMetadata (im.ItemSpec)));
var bytes = Encoding.UTF8.GetBytes (input.GetMetadata (im.ItemSpec));
writer.Write (bytes.Length);
writer.Write (bytes);
}
var hashBytes = sha.ComputeHash (buffer.ToArray ());
writer.Flush ();
buffer.Position = 0;
var hashBytes = sha.ComputeHash (buffer);
var hash = string.Join ("", hashBytes.Select (b => $"{b:x2}"));
input.SetMetadata (OutputMetadata, hash);
}
Expand Down
14 changes: 13 additions & 1 deletion msbuild/Xamarin.Shared/Xamarin.Shared.targets
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,10 @@ Copyright (C) 2018 Microsoft. All rights reserved.
* An Info.plist in their project file (by using a `None` item with filename "Info.plist" or with a `Link` metadata with filename "Info.plist"). We figure this out in the DetectAppManifest target.
* A partial plist in their project (using the `PartialAppManifest` item group). Developers can add targets to the public CollectAppManifestsDependsOn
property to run targets that add to the `PartialAppManifest` item group before we process them.
* Individual entries in the `AppManifestEntry` item group.
* Some MSBuild properties can also add values.

The precedence is: MSBuild properties can be overridden by the Info.plist, which can be overridden by a partial plist (a partial plist can also specify the "Overwrite=false" metadata to not overwrite any existing entries).
The precedence is: MSBuild properties can be overridden by the Info.plist, which can be overridden by a partial plist (a partial plist can also specify the "Overwrite=false" metadata to not overwrite any existing entries), which can be overridden by an AppManifestEntry item.

2. In the `CompileAppManifest` target we get all the inputs from above, and compute a temporary app manifest, which is written to a temporary output file.

Expand Down Expand Up @@ -622,7 +623,16 @@ Copyright (C) 2018 Microsoft. All rights reserved.
<Target Name="_CompileAppManifestInputs" Condition="'$(IsMacEnabled)' == 'true'" >
<ItemGroup>
<_FontFilesToRegister Include="@(BundleResource)" Condition="'%(BundleResource.RegisterFont)' == 'true'" />
<_AppManifestEntryToHash Include="@(AppManifestEntry)" />
<_AppManifestEntryHashMetadata Include="Identity;Type;Value;ArraySeparator" />
</ItemGroup>
<ComputeHashForItems
Input="@(_AppManifestEntryToHash)"
InputMetadata="@(_AppManifestEntryHashMetadata)"
OutputMetadata="_Hash"
>
<Output TaskParameter="Output" ItemName="_AppManifestEntryWithHash" />
</ComputeHashForItems>

<PropertyGroup>
<_CompileAppManifestInputFile>$(DeviceSpecificIntermediateOutputPath)_CompileAppManifest.inputs</_CompileAppManifestInputFile>
Expand All @@ -637,6 +647,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
<_CompileAppManifestInputLine Include="ApplicationTitle=$(ApplicationTitle)" />
<_CompileAppManifestInputLine Include="ApplicationVersion=$(ApplicationVersion)" />
<_CompileAppManifestInputLine Include="AppManifest=$(AppBundleManifest)" />
<_CompileAppManifestInputLine Include="AppManifestEntry=%(_AppManifestEntryWithHash._Hash)" />
<_CompileAppManifestInputLine Include="CompiledAppManifest=$(_TemporaryAppManifest)" />
<_CompileAppManifestInputLine Include="DefaultSdkVersion=$(_SdkVersion)" />
<_CompileAppManifestInputLine Include="FontFilesToRegister=$(_CompileAppManifestFontFilesToRegister)" />
Expand Down Expand Up @@ -687,6 +698,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
ApplicationTitle="$(ApplicationTitle)"
ApplicationVersion="$(ApplicationVersion)"
AppManifest="$(AppBundleManifest)"
AppManifestEntries="@(AppManifestEntry)"
BundleExecutable="$(_NativeExecutableName)"
CompiledAppManifest="$(_TemporaryAppManifest)"
DefaultSdkVersion="$(_SdkVersion)"
Expand Down
5 changes: 5 additions & 0 deletions tests/dotnet/MyPartialAppManifestApp/shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
<ApplicationTitle>MyPartialAppManifestApp</ApplicationTitle>
<ApplicationId>com.xamarin.mypartialappmanifestapp</ApplicationId>
<ApplicationVersion>3.14</ApplicationVersion>
<LaunchStoryboardName Condition="'$(LaunchStoryboardName)' == ''">LaunchScreen</LaunchStoryboardName>
<ManifestKey Condition="'$(ManifestKey)' == ''">a|String</ManifestKey>
<ManifestValue Condition="'$(ManifestValue)' == ''">b</ManifestValue>
</PropertyGroup>

<Import Project="../../common/shared-dotnet.csproj" />

<ItemGroup>
<Compile Include="../*.cs" />
<AppManifestEntry Include="$(ManifestKey)" Type="String" Value="$(ManifestValue)" />
<AppManifestEntry Include="UILaunchStoryboardName" Type="String" Value="$(LaunchStoryboardName)" />
<PartialAppManifest Include="../Partial.plist" />
</ItemGroup>
</Project>
18 changes: 18 additions & 0 deletions tests/dotnet/UnitTests/PartialAppManifestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public void Build (ApplePlatform platform, string runtimeIdentifiers)
Assert.That (infoPlist.GetString ("CFBundleVersion").Value, Is.EqualTo ("3.14"), "CFBundleVersion");
Assert.That (infoPlist.GetString ("CFBundleShortVersionString").Value, Is.EqualTo ("3.14"), "CFBundleShortVersionString");
Assert.That (infoPlist.GetString ("Something").Value, Is.EqualTo ("SomeValue"), "Something");
Assert.That (infoPlist.GetString ("UILaunchStoryboardName").Value, Is.EqualTo ("LaunchScreen"), "UILaunchStoryboardName");
Assert.That (infoPlist.GetString ("a|String").Value, Is.EqualTo ("b"), "Delimiter value");

var partialAppManifestPath = Path.Combine (Path.GetDirectoryName (project_path)!, "..", "Partial.plist");
Configuration.Touch (partialAppManifestPath);
Expand All @@ -33,6 +35,22 @@ public void Build (ApplePlatform platform, string runtimeIdentifiers)
rv = DotNet.AssertBuild (project_path, GetDefaultProperties (runtimeIdentifiers));
allTargets = BinLog.GetAllTargets (rv.BinLogPath);
AssertTargetNotExecuted (allTargets, "_CompileAppManifest", "_CompileAppManifest rebuild 2");

properties = GetDefaultProperties (runtimeIdentifiers);
properties ["ManifestKey"] = "a";
properties ["ManifestValue"] = "String|b";
rv = DotNet.AssertBuild (project_path, properties);
allTargets = BinLog.GetAllTargets (rv.BinLogPath);
AssertTargetExecuted (allTargets, "_CompileAppManifest", "_CompileAppManifest rebuild 3");

infoPlist = PDictionary.OpenFile (infoPlistPath);
Assert.That (infoPlist.GetString ("UILaunchStoryboardName").Value, Is.EqualTo ("LaunchScreen"), "UILaunchStoryboardName unchanged");
Assert.That (infoPlist.ContainsKey ("a|String"), Is.False, "Old delimiter key");
Assert.That (infoPlist.GetString ("a").Value, Is.EqualTo ("String|b"), "Updated delimiter value");

rv = DotNet.AssertBuild (project_path, properties);
allTargets = BinLog.GetAllTargets (rv.BinLogPath);
AssertTargetNotExecuted (allTargets, "_CompileAppManifest", "_CompileAppManifest rebuild 4");
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Utilities;
Expand Down Expand Up @@ -49,6 +50,61 @@ public void MainMinimumOSVersions ()
Assert.That (plist.GetMinimumOSVersion (), Is.EqualTo ("14.0"), "MinimumOSVersion");
}

[Test]
public void AppManifestEntries ()
{
var dir = Cache.CreateTemporaryDirectory ();
var task = CreateTask (dir);

var mainPath = Path.Combine (dir, "Info.plist");
var main = new PDictionary {
{ "StringValue", new PString ("main") },
{ "RemoveValue", new PString ("remove me") },
};
main.Save (mainPath);

var partialPath = Path.Combine (dir, "PartialAppManifest.plist");
var partial = new PDictionary {
{ "StringValue", new PString ("partial") },
};
partial.Save (partialPath);

task.AppManifest = new TaskItem (mainPath);
task.PartialAppManifests = [new TaskItem (partialPath)];
task.AppManifestEntries = [
new TaskItem ("StringValue", new Dictionary<string, string> { { "Type", "String" }, { "Value", "entry" } }),
new TaskItem ("BooleanValue", new Dictionary<string, string> { { "Type", "Boolean" }, { "Value", "TrUe" } }),
new TaskItem ("StringArrayValue", new Dictionary<string, string> { { "Type", "StringArray" }, { "Value", "a;b" } }),
new TaskItem ("CustomStringArrayValue", new Dictionary<string, string> { { "Type", "StringArray" }, { "Value", "c|d" }, { "ArraySeparator", "|" } }),
new TaskItem ("RemoveValue", new Dictionary<string, string> { { "Type", "Remove" } }),
];

ExecuteTask (task);

var plist = PDictionary.OpenFile (task.CompiledAppManifest!.ItemSpec);
Assert.That (plist.GetString ("StringValue").Value, Is.EqualTo ("entry"), "StringValue");
Assert.That (plist.Get<PBoolean> ("BooleanValue")?.Value, Is.True, "BooleanValue");
Assert.That (plist.GetArray ("StringArrayValue").OfType<PString> ().Select (v => v.Value), Is.EqualTo (new [] { "a", "b" }), "StringArrayValue");
Assert.That (plist.GetArray ("CustomStringArrayValue").OfType<PString> ().Select (v => v.Value), Is.EqualTo (new [] { "c", "d" }), "CustomStringArrayValue");
Assert.That (plist.ContainsKey ("RemoveValue"), Is.False, "RemoveValue");
}

[Test]
[TestCase ("Remove", "unexpected", "Invalid value 'unexpected' for the app manifest entry 'TestEntry' of type 'Remove' specified in the AppManifestEntry item group. Expected no value at all.")]
[TestCase ("Boolean", "not-a-boolean", "Invalid value 'not-a-boolean' for the app manifest entry 'TestEntry' of type 'Boolean' specified in the AppManifestEntry item group. Expected 'true' or 'false'.")]
[TestCase ("Unknown", "value", "Unknown type 'Unknown' for the app manifest entry 'TestEntry' specified in the AppManifestEntry item group. Expected 'Remove', 'Boolean', 'String', or 'StringArray'.")]
public void InvalidAppManifestEntry (string type, string value, string expectedError)
{
var task = CreateTask ();
task.AppManifestEntries = [
new TaskItem ("TestEntry", new Dictionary<string, string> { { "Type", type }, { "Value", value } }),
];

ExecuteTask (task, expectedErrorCount: 1);

Assert.That (Engine.Logger.ErrorEvents [0].Message, Is.EqualTo (expectedError));
}

[Test]
public void MultipleMinimumOSVersions ()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable

using System.Collections.Generic;

using Microsoft.Build.Utilities;
using NUnit.Framework;

namespace Xamarin.MacDev.Tasks {
[TestFixture]
public class ComputeHashForItemsTaskTests : TestBase {
[Test]
public void MetadataBoundariesAffectHash ()
{
var first = new TaskItem ("aString", new Dictionary<string, string> {
{ "Type", "String" },
{ "Value", "b" },
});
var second = new TaskItem ("a", new Dictionary<string, string> {
{ "Type", "String" },
{ "Value", "Stringb" },
});
var task = CreateTask<ComputeHashForItems> ();
task.Input = [first, second];
task.InputMetadata = [
new TaskItem ("Identity"),
new TaskItem ("Type"),
new TaskItem ("Value"),
new TaskItem ("ArraySeparator"),
];
task.OutputMetadata = "Hash";

ExecuteTask (task);

Assert.That (first.GetMetadata ("Hash"), Is.Not.EqualTo (second.GetMetadata ("Hash")));
}
}
}
Loading