xamarin / xamarin-macios

.NET for iOS, Mac Catalyst, macOS, and tvOS provide open-source bindings of the Apple SDKs for use with .NET managed languages such as C#
Other
2.44k stars 510 forks source link

[Feature Request] Add support for Attributes to build Info.plist #5916

Open dansiegel opened 5 years ago

dansiegel commented 5 years ago

Description

In Xamarin Android we have a number of really helpful attributes that can generally replace the need for creating complex Android Manifests. Some examples would include the UsesPermissionAttribute, IntentFilterAttribute, ApplicationAttribute, ActivityAttribute, etc. It's about high time that this sort of attribute love made it's way to iOS where we could start decorating our code with attributes that the build targets could then use to generate the full Info.plist or even to update the Entitilements.plist.

Example Use Case

A common use case for this would be when using App Center Distribution. Assuming a full CI/CD pipeline here I might have my app split up 3 times in App Center for Dev, Stage and Production. This means for each environment I've got a unique App Secret for App Center. What's more is I might want to use Distribution for Dev and Stage but not Production. If we look at the App Center Distribution docs we'll see that we need to add a CFBundleURLSchemes in the CFBundleURLTypes:

<key>CFBundleURLTypes</key>
  <array>
      <dict>
          <key>CFBundleURLSchemes</key>
          <array>
              <string>appcenter-${APP_SECRET}</string>
          </array>
      </dict>
  </array>

As an attribute this might look something like:

[assembly: CFBundleUrlType(Schemes = new[] { "appcenter-{app_secret}" })]

Or really for best practices it would probably look more like:

[assembly: CFBundleUrlType(Schemes = new[] { Constants.AppCenterScheme })]

The benefit here is that we can rely on processes we're probably are already using to inject things like the App Center secret at build time rather than having to deal with the nightmare of trying to make sure it doesn't get checked into source control and then writing a bunch of special build scripts to go and do regex replacements of some token in the Info.plist.

Proposed Attributes

public enum BundleUrlTypeRole
{
    None,
    Editor,
    Viewer
}

public class CFBundleUrlTypeAttribute : Attribute
{
    public BundleUrlTypeRole Role { get; set; }
    public string Name { get; set; }
    public string[] Schemes { get; set; }
    public string Icon { get; set; }
}

public class UIApplicationAttribute : Attribute
{
    public string Name { get; set; } // AwesomeApp
    public string DisplayName { get; set; } // Awesome App
    public string Identifier { get; set; }  // com.contoso.awesomeapp
    public string MinimumOSVersion { get; set; } // 10.0
    public string Version { get; set; } // 1.0.0.1234
    public string ShortVersion { get; set; } // 1.0
    public string[] Fonts { get; set; } 
    public bool AutoLoadFonts { get; set; } // if true add any ttf/otf that is a BundleResource
}

public static class PrivacyUsage
{
    public const string Camera = nameof(Camera);
    public const string Contacts = nameof(Contacts);
    public const string Health = nameof(Health);
    public const string LocationAlways = nameof(LocationAlways);
    public const string Location = nameof(Location);
    public const string LocationWhenInUse = nameof(LocationWhenInUse);
    // etc...
}

public class PrivacyAttribute : Attribute
{
    public PrivatcyAttribute(string permission, string description) { }

    public string Permission { get; } // i.e. Camera
    public string Description { get; }  // We want to take fun photos of your dinner for Instagram
}

public enum ApsEnvironment
{
    Development,
    Production
}

public class PushNotificationAttribute : Attribute
{
    public PushNotificationAttribute(ApsEnvironment environment) { }
    public ApsEnvironment Environment { get; }
}

// Assumed to be enabled if the attribute exists
public class DataProtectionAttribute : Attribute
{
}

public class KeychainAttribute : Attribute
{
    public KeychainAttribute(string[] groups) { }
    public string[] Groups { get; }
}

public class AssociatedDomainsAttribute
 : Attribute
{
    public AssociatedDomainsAttribute
(string[] domains) { }
    public string[] Domains { get; }
}

public class AppGroupsAttribute : Attribute
{
    public AppGroupsAttribute (string[] domains) { }
    public string[] Domains { get; }
}

Putting it all together

To put this all together you might have something like the following to handle the above Use case in which you have an app with 3 environments Dev, Stage, & Production:

// Properties like the Version, Min OS Version would not be touched in the 
// Info.plist because they are not set here.
#if STAGE
[assembly: UIApplication(
  Name = "AwesomeAppStage",
  DisplayName = "Awesome App (Stage)",
  Identifier = "com.contoso.awesomeapp-stage",
  AutoLoadFonts = true
)]
#elif STORE
[assembly: UIApplication(
  Name = "AwesomeApp",
  DisplayName = "Awesome App",
  Identifier = "com.contoso.awesomeapp",
  AutoLoadFonts = true
)]
#else
[assembly: UIApplication(
  Name = "AwesomeAppDev",
  DisplayName = "Awesome App (Dev)",
  Identifier = "com.contoso.awesomeapp-dev",
  AutoLoadFonts = true
)]
#endif

#if STORE
[assembly: PushNotification(ApsEnvironment.Production)]
#else
[assembly: PushNotification(ApsEnvironment.Development)]
[assembly: CFBundleUrlType(Schemes = new[] { Constants.AppCenterScheme })]
#endif

[assembly: DataProtection]
[assembly: Keychain(new[] { "com.contoso.awesomeapp" })]

[assembly: Privacy(PrivacyUsage.Camera, "We need to scan a barcode")]
[assembly: Privacy(PrivacyUsage.Location, "We track your movements and report them to your boss")]

At build time there should be a build task that transforms both the Info.plist and Entitlements.plist based on these attributes so that you are in essence updating your plist from code and without the need for crazy build scripts.

spouliot commented 5 years ago

I love the idea :)

They (attributes) needs to come up with rules for what happens if

We can already limit usage to single use in assemblies - for a singe attribute. However something like

[assembly: Privacy(PrivacyUsage.Camera, "We need to scan a barcode")]
[assembly: Privacy(PrivacyUsage.Location, "We track your movements and report them to your boss")]

must be allowed so we also need a rule for

[assembly: Privacy(PrivacyUsage.Camera, "We need to scan a barcode")]
[assembly: Privacy(PrivacyUsage.Camera, "We need your picture too")]
dansiegel commented 5 years ago

Info.plist already has values;

I would think that in this case we want to overwrite the value in the Plist… Perhaps this could be surfaced with a build warning

if more than one assembly has the attributes;

I would think we would only scan the assembly of the project that contains the Info.plist/Entitlements.plist so you wouldn't have a multi-assembly issue there.

(paraphrased) Duplicate Privacy Attributes with the same PrivateUsage

I'd have to ask (because I honestly have never tried) what would happen if you tried to add the duplicate keys in the Info.plist? Is there a process currently that would catch this?

That said I would think we want to surface this in the build output as either an Error or Warning that specifies what the duplicated PrivacyUsage is and what the descriptions of each are.

dansiegel commented 5 years ago

@spouliot I've put together a POC repo. I'm still trying to work out where to inject the build task correctly. I'd welcome any feedback you may have there. Perhaps we can put this out as a POC and develop the feature there then bring it over?

rolfbjarne commented 5 years ago

Do we want to link away these attributes? They're not really needed after the build.

dansiegel commented 5 years ago

@rolfbjarne I would tend to agree... I can't see any harm in linking them away once the build task has run.

rolfbjarne commented 5 years ago

It would probably make most sense to put this logic in https://github.com/xamarin/xamarin-macios/blob/master/msbuild/Xamarin.iOS.Tasks.Core/Tasks/CompileAppManifestTaskBase.cs then. I'm not sure how to inject any custom logic there without modifying our build though.

jennyf19 commented 4 years ago

@rolfbjarne @spouliot Any update on this feature request?

rolfbjarne commented 4 years ago

@jennyf19 Unfortunately no, there's no progress/news about this, we haven't had time to look at it.

jennyf19 commented 4 years ago

Thanks for the update @rolfbjarne, we'd love to see this feature to make our samples easier to use. Hopefully it will get added to the backlog soon.

cc: @TiagoBrenck

jennyf19 commented 4 years ago

@rolfbjarne Any update? thanks.

rolfbjarne commented 4 years ago

@jennyf19 I'm sorry to say we have no updates since last time, we've been busy with other tasks.

dansiegel commented 6 months ago

Since we're coming up on the 5 year mark... any chance we could sneak this in for net9.0?

rolfbjarne commented 3 months ago

Here's a potential plan:

  1. Add support for specifying entries into the app manifest (Info.plist) using an MSBuild item group. This could be modeled after our current support for CustomEntitlements, so something like this:

    <ItemGroup>
        <AppManifestEntry Include="CFSomething" Type="Boolean" Value="true" /> <!-- value can be 'false' too (case doesn't matter) -->
        <AppManifestEntry Include="CFSomething" Type="String" Value="stringvalue" />
        <AppManifestEntry Include="CFSomething" Type="StringArray" Value="a;b" /> <!-- array of strings, separated by semicolon -->
        <AppManifestEntry Include="CFSomething" Type="StringArray" Value="a😁b" ArraySeparator="😁" /> <!-- array of strings, separated by 😁 -->
        <AppManifestEntry Include="CFSomething" Type="Remove" /> <!-- This will remove the corresponding entry  -->
    </ItemGroup>
  2. Add a pre-linker MSBuild task that looks for any assembly attribute we want to support and emits these AppManifestEntry items.

Note: we could run the MSBuild task post-linker, but then we wouldn't be able to remove these attributes in the linker. The downside of running it pre-linker is that we might end up adding app manifest entries from assemblies that aren't even reachable from the executable project.

Another alternative is to extract all the information about the attributes before the linker runs, and then check which assemblies survived linking, and only for those add the AppManifestEntry items to the build.

Moving tentatively to the .NET 9 milestone, but no guarantees!