Skip to content

Commit

Permalink
[manifest-attribute-codegen] Generate custom attribute declarations (#…
Browse files Browse the repository at this point in the history
…8781)

Fixes: #8272

Context: #8235
Context: #8729
Context: e790874

Previously, we did not have an established process for detecting new
XML elements and attributes allowed in `AndroidManifest.xml` and
surfacing them to users via our manifest attributes like
`[ActivityAttribute]`.  This leads to users having to use manual
workarounds until our attributes can be updated.

Additionally, whenever we do add new properties to these attributes,
it requires manually updating multiple files by hand that must remain
in sync, eg:

  * [src/Mono.Android/Android.App/IntentFilterAttribute.cs](https://github.com/xamarin/xamarin-android/blob/180dd5205ab270bb74bb853754665db9cb5d65f1/src/Mono.Android/Android.App/IntentFilterAttribute.cs#L9)
  * [src/Xamarin.Android.Build.Tasks/Mono.Android/IntentFilterAttribute.Partial.cs](https://github.com/xamarin/xamarin-android/blob/180dd5205ab270bb74bb853754665db9cb5d65f1/src/Xamarin.Android.Build.Tasks/Mono.Android/IntentFilterAttribute.Partial.cs#L14)

The `build-tools/manifest-attribute-codegen` utility (e790874) has
support to parse Android SDK `attrs_manifest.xml` files, which
specifies what elements and attributes are valid within
`AndroidManifest.xml`.

Update `manifest-attribute-codegen` to do what it's name already
implied: generate code!  It now reads a `metadata.xml` file which
controls which custom attributes to emit, where to emit them, and
what members those custom attributes should have (among other things).
This makes it easier to ensure that code shared by `src/Mono.Android`
and `src/Xamarin.Android.Build.Tasks` are consistent, meaking it
easier to correctly add support for new attributes and/or
attribute members.

Generated file semantics and naming conventions: consider the C# type
`Android.App.ActivityAttribute`.

  * `src\Xamarin.Android.NamingCustomAttributes\Android.App\ActivityAttribute.cs`
    contains the C# `partial` class declaration that can be shared
    by both `src\Mono.Android` and `src\Xamarin.Android.Build.Tasks`.
    This file also contains a `#if XABT_MANIFEST_EXTENSIONS` block
    which is only used by `src\Xamarin.Android.Build.Tasks`.

  * `src/Xamarin.Android.Build.Tasks/Mono.Android/ActivityAttribute.Partial.cs`
    contains the C# `partial` class declaration with code specific
    to `Xamarin.Android.Build.Tasks.dll`.

  * `src/Xamarin.Android.NamingCustomAttributes/Android.App/ActivityAttribute.Partial.cs`
    contains the C# `partial` class declaration with code specific
    to `Mono.Android.dll`.

`metadata.xml` contents and the update process is documented in
`build-tools/manifest-attribute-codegen/README.md`.

Also removed the `ANDROID_*` values from `$(DefineConstants)` for
`Xamarin.Android.Build.Tasks.csproj` as we no longer build separate
assemblies for old Android API levels.

Note this commit does not change any existing manifest attributes or
the properties they expose.  It merely generates what we expose today.
We will determine additional properties to expose in a future commit.
  • Loading branch information
jpobst authored Jun 7, 2024
1 parent b994441 commit 3ab74db
Show file tree
Hide file tree
Showing 63 changed files with 3,673 additions and 1,882 deletions.
5 changes: 5 additions & 0 deletions Documentation/workflow/HowToAddNewApiLevel.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ This will create a `api-XX.xml` file in `/src/Mono.Android/Profiles/` that needs
- Add required metadata fixes in `/src/Mono.Android/metadata` until `Mono.Android.csproj` builds
- Check that new package/namespaces are properly cased

### New AndroidManifest.xml Elements

- See `build-tools/manifest-attribute-codegen/README.md` for instructions on surfacing any new
elements or attributes added to `AndroidManifest.xml`.

### ApiCompat

There may be ApiCompat issues that need to be examined. Either fix the assembly with metadata or allow
Expand Down
142 changes: 142 additions & 0 deletions build-tools/manifest-attribute-codegen/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Xml.Linq;
using Xamarin.SourceWriter;

namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;

static class StringExtensions
{
static StringExtensions ()
{
// micro unit testing, am so clever!
if (Hyphenate ("AndSoOn") != "and-so-on")
throw new InvalidOperationException ("Am so buggy 1 " + Hyphenate ("AndSoOn"));
if (Hyphenate ("aBigProblem") != "a-big-problem")
throw new InvalidOperationException ("Am so buggy 2");
if (Hyphenate ("my-two-cents") != "my-two-cents")
throw new InvalidOperationException ("Am so buggy 3");
}

public static string Hyphenate (this string s)
{
var sb = new StringBuilder (s.Length * 2);
for (int i = 0; i < s.Length; i++) {
if (char.IsUpper (s [i])) {
if (i > 0)
sb.Append ('-');
sb.Append (char.ToLowerInvariant (s [i]));
} else
sb.Append (s [i]);
}
return sb.ToString ();
}

const string prefix = "AndroidManifest";

public static string ToActualName (this string s)
{
s = s.IndexOf ('.') < 0 ? s : s.Substring (s.LastIndexOf ('.') + 1);

var ret = (s.StartsWith (prefix, StringComparison.Ordinal) ? s.Substring (prefix.Length) : s).Hyphenate ();
return ret.Length == 0 ? "manifest" : ret;
}

public static bool GetAttributeBoolOrDefault (this XElement element, string attribute, bool defaultValue)
{
var value = element.Attribute (attribute)?.Value;

if (value is null)
return defaultValue;

if (bool.TryParse (value, out var ret))
return ret;

return defaultValue;
}

public static string GetRequiredAttributeString (this XElement element, string attribute)
{
var value = element.Attribute (attribute)?.Value;

if (value is null)
throw new InvalidDataException ($"Missing '{attribute}' attribute.");

return value;
}

public static string GetAttributeStringOrEmpty (this XElement element, string attribute)
=> element.Attribute (attribute)?.Value ?? string.Empty;

public static string Unhyphenate (this string s)
{
if (s.IndexOf ('-') < 0)
return s;

var sb = new StringBuilder ();

for (var i = 0; i < s.Length; i++) {
if (s [i] == '-') {
sb.Append (char.ToUpper (s [i + 1]));
i++;
} else {
sb.Append (s [i]);
}
}

return sb.ToString ();
}

public static string Capitalize (this string s)
{
return char.ToUpper (s [0]) + s.Substring (1);
}

public static void WriteAutoGeneratedHeader (this CodeWriter sw)
{
sw.WriteLine ("//------------------------------------------------------------------------------");
sw.WriteLine ("// <auto-generated>");
sw.WriteLine ("// This code was generated by 'manifest-attribute-codegen'.");
sw.WriteLine ("//");
sw.WriteLine ("// Changes to this file may cause incorrect behavior and will be lost if");
sw.WriteLine ("// the code is regenerated.");
sw.WriteLine ("// </auto-generated>");
sw.WriteLine ("//------------------------------------------------------------------------------");
sw.WriteLine ();
sw.WriteLine ("#nullable enable"); // Roslyn turns off NRT for generated files by default, re-enable it
}

/// <summary>
/// Returns the first subset of a delimited string. ("127.0.0.1" -> "127")
/// </summary>
[return: NotNullIfNotNull (nameof (s))]
public static string? FirstSubset (this string? s, char separator)
{
if (!s.HasValue ())
return s;

var index = s.IndexOf (separator);

if (index < 0)
return s;

return s.Substring (0, index);
}

/// <summary>
/// Returns the final subset of a delimited string. ("127.0.0.1" -> "1")
/// </summary>
[return: NotNullIfNotNull (nameof (s))]
public static string? LastSubset (this string? s, char separator)
{
if (!s.HasValue ())
return s;

var index = s.LastIndexOf (separator);

if (index < 0)
return s;

return s.Substring (index + 1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Xml.Linq;
using Xamarin.SourceWriter;

namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;

class AttributeDefinition
{
public string ApiLevel { get; }
public string Name { get; }
public string Format { get; }
public List<EnumDefinition> Enums { get; } = new List<EnumDefinition> ();

public AttributeDefinition (string apiLevel, string name, string format)
{
ApiLevel = apiLevel;
Name = name;
Format = format;
}

public string GetAttributeType ()
{
return Format switch {
"boolean" => "bool",
"integer" => "int",
"string" => "string?",
_ => "string?",
};
}

public static AttributeDefinition FromElement (string api, XElement e)
{
var name = e.GetAttributeStringOrEmpty ("name");
var format = e.GetAttributeStringOrEmpty ("format");

var def = new AttributeDefinition (api, name, format);

var enums = e.Elements ("enum")
.Select (n => new EnumDefinition (api, n.GetAttributeStringOrEmpty ("name"), n.GetAttributeStringOrEmpty ("value")));

def.Enums.AddRange (enums);

return def;
}

public void WriteXml (TextWriter w)
{
var format = Format.HasValue () ? $" format='{Format}'" : string.Empty;
var api_level = int.TryParse (ApiLevel, out var level) && level <= 10 ? string.Empty : $" api-level='{ApiLevel}'";

w.Write ($" <a name='{Name}'{format}{api_level}");

if (Enums.Count > 0) {
w.WriteLine (">");
foreach (var e in Enums)
w.WriteLine ($" <enum-definition name='{e.Name}' value='{e.Value}' api-level='{e.ApiLevel}' />");
w.WriteLine (" </a>");
} else
w.WriteLine (" />");
}
}
50 changes: 50 additions & 0 deletions build-tools/manifest-attribute-codegen/Models/ElementDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Xml.Linq;

namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;

class ElementDefinition
{
static readonly char [] sep = [' '];

public string ApiLevel { get; }
public string Name { get; }
public string[]? Parents { get;}
public List<AttributeDefinition> Attributes { get; } = new List<AttributeDefinition> ();

public string ActualElementName => Name.ToActualName ();

public ElementDefinition (string apiLevel, string name, string []? parents)
{
ApiLevel = apiLevel;
Name = name;
Parents = parents;
}

public static ElementDefinition FromElement (string api, XElement e)
{
var name = e.GetAttributeStringOrEmpty ("name");
var parents = e.Attribute ("parent")?.Value?.Split (sep, StringSplitOptions.RemoveEmptyEntries);
var def = new ElementDefinition (api, name, parents);

var attrs = e.Elements ("attr")
.Select (a => AttributeDefinition.FromElement (api, a));

def.Attributes.AddRange (attrs);

return def;
}

public void WriteXml (TextWriter w)
{
w.WriteLine ($" <e name='{ActualElementName}' api-level='{ApiLevel}'>");

if (Parents?.Any () == true)
foreach (var p in Parents)
w.WriteLine ($" <parent>{p.ToActualName ()}</parent>");

foreach (var a in Attributes)
a.WriteXml (w);

w.WriteLine (" </e>");
}
}
15 changes: 15 additions & 0 deletions build-tools/manifest-attribute-codegen/Models/EnumDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;

class EnumDefinition
{
public string ApiLevel { get; set; }
public string Name { get; set; }
public string Value { get; set; }

public EnumDefinition (string apiLevel, string name, string value)
{
ApiLevel = apiLevel;
Name = name;
Value = value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System.Xml.Linq;

namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;

class ManifestDefinition
{
public string ApiLevel { get; set; } = "0";
public List<ElementDefinition> Elements { get; } = new List<ElementDefinition> ();

// Creates a new ManifestDefinition for a single Android API from the given file path
public static ManifestDefinition FromFile (string filePath)
{
var dir_name = new FileInfo (filePath).Directory?.Parent?.Parent?.Parent?.Name;

if (dir_name is null)
throw new InvalidOperationException ($"Could not determine API level from {filePath}");

var manifest = new ManifestDefinition () {
ApiLevel = dir_name.Substring (dir_name.IndexOf ('-') + 1)
};

var elements = XDocument.Load (filePath).Root?.Elements ("declare-styleable")
.Select (e => ElementDefinition.FromElement (manifest.ApiLevel, e))
.ToList ();

if (elements is not null)
manifest.Elements.AddRange (elements);

return manifest;
}

public static ManifestDefinition FromSdkDirectory (string sdkPath)
{
// Load all the attrs_manifest.xml files from the Android SDK
var manifests = Directory.GetDirectories (Path.Combine (sdkPath, "platforms"), "android-*")
.Select (d => Path.Combine (d, "data", "res", "values", "attrs_manifest.xml"))
.Where (File.Exists)
.Order ()
.Select (FromFile)
.ToList ();

// Merge all the manifests into a single one
var merged = new ManifestDefinition ();

foreach (var def in manifests) {
foreach (var el in def.Elements) {
var element = merged.Elements.FirstOrDefault (_ => _.ActualElementName == el.ActualElementName);
if (element == null)
merged.Elements.Add (element = new ElementDefinition (
el.ApiLevel,
el.Name,
(string []?) el.Parents?.Clone ()
));
foreach (var at in el.Attributes) {
var attribute = element.Attributes.FirstOrDefault (_ => _.Name == at.Name);
if (attribute == null)
element.Attributes.Add (attribute = new AttributeDefinition (
at.ApiLevel,
at.Name,
at.Format
));
foreach (var en in at.Enums) {
var enumeration = at.Enums.FirstOrDefault (_ => _.Name == en.Name);
if (enumeration == null)
attribute.Enums.Add (new EnumDefinition (
en.ApiLevel,
en.Name,
en.Value
));
}
}
}
}

return merged;
}

public void WriteXml (TextWriter w)
{
w.WriteLine ("<m>");

foreach (var e in Elements)
e.WriteXml (w);

w.WriteLine ("</m>");
}
}
Loading

0 comments on commit 3ab74db

Please sign in to comment.