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

[msbuild] Add support for alternate app icons. #21475

Merged
merged 4 commits into from
Nov 4, 2024
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
21 changes: 21 additions & 0 deletions docs/build-apps/build-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ ms.date: 09/19/2024
Build items control how .NET for iOS, Mac Catalyst, macOS, and tvOS
application or library projects are built.

## AlternateAppIcon

The `AlternateAppIcon` item group can be used to specify alternate app icons.

The `Include` metadata must point to the filename of an `.appiconset` (for
iOS, macOS and Mac Catalyst) or `.imagestack` (for tvOS) image resource
inside an asset catalog.

Example:

```xml
<ItemGroup>
<!-- The value to put in here for the "Resources/MyImages.xcassets/MyAlternateAppIcon.appiconset" resource would be "MyAlternateAppIcon" -->
<AlternateAppIcon Include="MyAlternateAppIcon" />
</ItemGroup>
```

See also:
* The [AppIcon](build-properties.md#AppIcon) property.
* The [IncludeAllAppIcons](build-properties.md#IncludeAllAppIcons) property.

## PartialAppManifest

`PartialAppManifest` can be used to add additional partial app manifests that
Expand Down
40 changes: 40 additions & 0 deletions docs/build-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,52 @@ MSBuild properties control the behavior of the
They're specified within the project file, for example **MyApp.csproj**, within
an MSBuild PropertyGroup.

## AppIcon

The `AppIcon` item group can be used to specify an app icon for the app.

The value of the property must point to the filename of an `.appiconset` (for
iOS, macOS and Mac Catalyst) or `.brandassets` (for tvOS) image resource
inside an asset catalog.

Example:

```xml
<PropertyGroup>
<!-- The value to put in here for the "Resources/MyImages.xcassets/MyAppIcon.appiconset" resource would be "MyAppIcon" -->
<AppIcon>MyAppIcon</AppIcon>
</PropertyGroup>
```

See also:

* The [AlternateAppIcon](build-items.md#AlternateAppIcon) item group.
* The [IncludeAllAppIcons](#IncludeAllAppIcons) property.

## DittoPath

The full path to the `ditto` executable.

The default behavior is to use `/usr/bin/ditto`.

## IncludeAllAppIcons

Set the `IncludeAllAppIcons` property to true to automatically include all app
icons from all asset catalogs in the app.

Example:

```xml
<PropertyGroup>
<IncludeAllAppIcons>true</IncludeAllAppIcons>
</PropertyGroup>
```

See also:

* The [AlternateAppIcon](build-items.md#AlternateAppIcon) item group.
* The [AppIcon](#AppIcon) property.

## MaciOSPrepareForBuildDependsOn

A semi-colon delimited property that can be used to extend the build process.
Expand Down
34 changes: 34 additions & 0 deletions msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,40 @@
<comment>SupportedOSPlatformVersion: don't translate (it's the name of an MSBuild property)</comment>
</data>

<data name="E7127" xml:space="preserve">
<value>Can't find the AlternateAppIcon '{0}' among the image resources. There are {1} app icons in the image resources: {2}.</value>
<comment>
* AlternateAppIcon: don't translate (it's the name of an MSBuild property)
* {1}: count of image resources
* {2}: comma separated list of resources.
</comment>
</data>

<data name="E7128" xml:space="preserve">
<value>The image resource '{0}' is specified as both 'AppIcon' and 'AlternateAppIcon'.</value>
<comment>
* AppIcon: don't translate (it's the name of an MSBuild property)
* AlternateAppIcon: don't translate (it's the name of an MSBuild property)
</comment>
</data>

<data name="E7129" xml:space="preserve">
<value>Can't specify both 'XSAppIconAssets' in the Info.plist and 'AppIcon' in the project file. Please select one or the other.</value>
<comment>
* XSAppIconAssets: don't translate (it's the name of an MSBuild property)
* AppIcon: don't translate (it's the name of an MSBuild property)
</comment>
</data>

<data name="E7130" xml:space="preserve">
<value>Can't find the AppIcon '{0}' among the image resources. There are {1} app icons in the image resources: {2}.</value>
<comment>
* AppIcon: don't translate (it's the name of an MSBuild property)
* {1}: count of image resources
* {2}: comma separated list of resources.
</comment>
</data>

<data name="E7131" xml:space="preserve">
<value>The source '{0}' does not exist.</value>
<comment>{0}: path to a file or a directory</comment>
Expand Down
103 changes: 82 additions & 21 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/ACTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@

namespace Xamarin.MacDev.Tasks {
public class ACTool : XcodeCompilerToolTask, ICancelableTask {
ITaskItem? partialAppManifest;
string? outputSpecs;
string? partialAppManifestPath;

#region Inputs

public string AccentColor { get; set; } = string.Empty;

public ITaskItem [] AlternateAppIcons { get; set; } = Array.Empty<ITaskItem> ();

// The name of an app icon
public string AppIcon { get; set; } = string.Empty;

public string DeviceModel { get; set; } = string.Empty;

public string DeviceOSVersion { get; set; } = string.Empty;
Expand All @@ -29,6 +34,8 @@ public class ACTool : XcodeCompilerToolTask, ICancelableTask {
[Required]
public ITaskItem [] ImageAssets { get; set; } = Array.Empty<ITaskItem> ();

public bool IncludeAllAppIcons { get; set; }

public bool IsWatchApp { get; set; }

[Required]
Expand All @@ -46,6 +53,11 @@ public class ACTool : XcodeCompilerToolTask, ICancelableTask {

#endregion

// All the icons among the 'ImageAssets'.
HashSet<string> appIconsInAssets = new (); // iOS, macOS and Mac Catalyst
HashSet<string> brandAssetsInAssets = new (); // tvOS
HashSet<string> imageStacksInAssets = new (); // tvOS

protected override string DefaultBinDir {
get { return DeveloperRootBinDir; }
}
Expand All @@ -64,6 +76,11 @@ protected override void AppendCommandLineArguments (IDictionary<string, string?>
{
var assetDirs = new HashSet<string> (items.Select (x => BundleResource.GetVirtualProjectPath (ProjectDir, x, !string.IsNullOrEmpty (SessionId))));

if (!string.IsNullOrEmpty (XSAppIconAssets) && !string.IsNullOrEmpty (AppIcon)) {
Log.LogError (MSBStrings.E7129 /* Can't specify both 'XSAppIconAssets' in the Info.plist and 'AppIcon' in the project file. Please select one or the other. */);
return;
}

if (!string.IsNullOrEmpty (XSAppIconAssets)) {
int index = XSAppIconAssets.IndexOf (".xcassets" + Path.DirectorySeparatorChar, StringComparison.Ordinal);
string? assetDir = null;
Expand All @@ -75,13 +92,6 @@ protected override void AppendCommandLineArguments (IDictionary<string, string?>
if (assetDirs is not null && assetDir is not null && assetDirs.Contains (assetDir)) {
var assetName = Path.GetFileNameWithoutExtension (rpath);

if (PartialAppManifest is null && partialAppManifest is not null) {
args.Add ("--output-partial-info-plist");
args.AddQuoted (partialAppManifest.GetMetadata ("FullPath"));

PartialAppManifest = partialAppManifest;
}

args.Add ("--app-icon");
args.AddQuoted (assetName);

Expand All @@ -104,14 +114,6 @@ protected override void AppendCommandLineArguments (IDictionary<string, string?>

if (assetDirs is not null && assetDir is not null && assetDirs.Contains (assetDir)) {
var assetName = Path.GetFileNameWithoutExtension (rpath);

if (PartialAppManifest is null && partialAppManifest is not null) {
args.Add ("--output-partial-info-plist");
args.AddQuoted (partialAppManifest.GetMetadata ("FullPath"));

PartialAppManifest = partialAppManifest;
}

args.Add ("--launch-image");
args.AddQuoted (assetName);
}
Expand Down Expand Up @@ -147,12 +149,57 @@ protected override void AppendCommandLineArguments (IDictionary<string, string?>
foreach (var targetDevice in GetTargetDevices ())
args.Add ("--target-device", targetDevice);

args.Add ("--minimum-deployment-target", MinimumOSVersion);
if (!string.IsNullOrEmpty (MinimumOSVersion))
args.Add ("--minimum-deployment-target", MinimumOSVersion);

var platform = PlatformUtils.GetTargetPlatform (SdkPlatform, IsWatchApp);

if (platform is not null)
args.Add ("--platform", platform);

if (!string.IsNullOrEmpty (AppIcon)) {
if (Platform == ApplePlatform.TVOS) {
if (!brandAssetsInAssets.Contains (AppIcon)) {
Log.LogError (MSBStrings.E7130 /* Can't find the AppIcon '{0}' among the image resources. There are {1} app icons in the image resources: {2} */, AppIcon, brandAssetsInAssets.Count, string.Join (", ", brandAssetsInAssets.OrderBy (v => v)));
return;
}
} else {
if (!appIconsInAssets.Contains (AppIcon)) {
Log.LogError (MSBStrings.E7130 /* Can't find the AppIcon '{0}' among the image resources. There are {1} app icons in the image resources: {2} */, AppIcon, appIconsInAssets.Count, string.Join (", ", appIconsInAssets.OrderBy (v => v)));
return;
}
}
args.Add ("--app-icon");
args.AddQuoted (AppIcon);
}

foreach (var alternate in AlternateAppIcons) {
var alternateAppIcon = alternate.ItemSpec!;
if (Platform == ApplePlatform.TVOS) {
if (!imageStacksInAssets.Contains (alternateAppIcon)) {
Log.LogError (MSBStrings.E7127 /* Can't find the AlternateAppIcon '{0}' among the image resources. There are {1} app icons in the image resources: {2}. */, alternateAppIcon, imageStacksInAssets.Count, string.Join (", ", imageStacksInAssets.OrderBy (v => v)));
return;
}
} else {
if (!appIconsInAssets.Contains (alternateAppIcon)) {
Log.LogError (MSBStrings.E7127 /* Can't find the AlternateAppIcon '{0}' among the image resources. There are {1} app icons in the image resources: {2}. */, alternateAppIcon, appIconsInAssets.Count, string.Join (", ", appIconsInAssets.OrderBy (v => v)));
return;
}
}
if (string.Equals (alternateAppIcon, AppIcon, StringComparison.OrdinalIgnoreCase)) {
Log.LogError (MSBStrings.E7128 /* The image resource '{0}' is specified as both 'AppIcon' and 'AlternateAppIcon'. */, AppIcon);
return;
}
// This doesn't seem to be necessary/applicable for tvOS (it also triggers a warning from actool)
args.Add ("--alternate-app-icon");
args.AddQuoted (alternateAppIcon);
}

if (IncludeAllAppIcons)
args.Add ("--include-all-app-icons");

args.Add ("--output-partial-info-plist");
args.AddQuoted (Path.GetFullPath (partialAppManifestPath));
}

IEnumerable<ITaskItem> GetCompiledBundleResources (PDictionary output, string intermediateBundleDir)
Expand Down Expand Up @@ -309,6 +356,17 @@ public override bool Execute ()
var catalog = Path.GetDirectoryName (vpath);
path = Path.GetDirectoryName (path);

if (Platform == ApplePlatform.TVOS) {
if (path.EndsWith (".imagestack", StringComparison.OrdinalIgnoreCase)) {
imageStacksInAssets.Add (Path.GetFileNameWithoutExtension (path));
} else if (path.EndsWith (".brandassets", StringComparison.OrdinalIgnoreCase)) {
brandAssetsInAssets.Add (Path.GetFileNameWithoutExtension (path));
}
} else {
if (path.EndsWith (".appiconset", StringComparison.OrdinalIgnoreCase))
appIconsInAssets.Add (Path.GetFileNameWithoutExtension (path));
}

// keep walking up the directory structure until we get to the .xcassets directory
while (!string.IsNullOrEmpty (catalog) && Path.GetExtension (catalog) != ".xcassets") {
catalog = Path.GetDirectoryName (catalog);
Expand Down Expand Up @@ -391,7 +449,8 @@ public override bool Execute ()
return !Log.HasLoggedErrors;
}

partialAppManifest = new TaskItem (Path.Combine (intermediate, "partial-info.plist"));
partialAppManifestPath = Path.Combine (intermediate, "partial-info.plist");
PartialAppManifest = new TaskItem (partialAppManifestPath);

if (specs.Count > 0) {
outputSpecs = Path.Combine (intermediate, "output-specifications.plist");
Expand All @@ -400,12 +459,14 @@ public override bool Execute ()

Directory.CreateDirectory (intermediateBundleDir);

// Note: Compile() will set the PartialAppManifest property if it is used...
if ((Compile (catalogs.ToArray (), intermediateBundleDir, manifest)) != 0)
return false;

if (PartialAppManifest is not null && !File.Exists (PartialAppManifest.GetMetadata ("FullPath")))
Log.LogError (MSBStrings.E0093, PartialAppManifest.GetMetadata ("FullPath"));
if (Log.HasLoggedErrors)
return false;

if (!File.Exists (Path.GetFullPath (partialAppManifestPath)))
Log.LogError (MSBStrings.E0093, Path.GetFullPath (partialAppManifestPath));

try {
var manifestOutput = PDictionary.FromFile (manifest.ItemSpec)!;
Expand Down
5 changes: 5 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ protected int Compile (ITaskItem [] items, string output, ITaskItem manifest)
args.AddQuoted (item.GetMetadata ("FullPath"));

var arguments = args.ToList ();

// don't bother executing the tool if we've already looged errors.
if (Log.HasLoggedErrors)
return 1;

var rv = ExecuteAsync (tool, arguments, sdkDevPath, environment: environment, mergeOutput: false).Result;
var exitCode = rv.ExitCode;
var messages = rv.StandardOutput!.ToString ();
Expand Down
3 changes: 3 additions & 0 deletions msbuild/Xamarin.Shared/Xamarin.Shared.targets
Original file line number Diff line number Diff line change
Expand Up @@ -910,12 +910,15 @@ Copyright (C) 2018 Microsoft. All rights reserved.
ToolExe="$(ACToolExe)"
ToolPath="$(ACToolPath)"
AccentColor="$(AccentColor)"
AlternateAppIcons="@(AlternateAppIcon)"
AppIcon="$(AppIcon)"
BundleIdentifier="$(_BundleIdentifier)"
CLKComplicationGroup="$(_CLKComplicationGroup)"
DeviceModel="$(TargetDeviceModel)"
DeviceOSVersion="$(TargetDeviceOSVersion)"
EnableOnDemandResources="$(EnableOnDemandResources)"
ImageAssets="@(ImageAsset)"
IncludeAllAppIcons="$(IncludeAllAppIcons)"
MinimumOSVersion="$(_MinimumOSVersion)"
NSExtensionPointIdentifier="$(_NSExtensionPointIdentifier)"
OptimizePNGs="$(OptimizePNGs)"
Expand Down
Loading