Skip to content

Commit 3ab74db

Browse files
authored
[manifest-attribute-codegen] Generate custom attribute declarations (#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.
1 parent b994441 commit 3ab74db

File tree

63 files changed

+3673
-1882
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+3673
-1882
lines changed

Documentation/workflow/HowToAddNewApiLevel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ This will create a `api-XX.xml` file in `/src/Mono.Android/Profiles/` that needs
5858
- Add required metadata fixes in `/src/Mono.Android/metadata` until `Mono.Android.csproj` builds
5959
- Check that new package/namespaces are properly cased
6060

61+
### New AndroidManifest.xml Elements
62+
63+
- See `build-tools/manifest-attribute-codegen/README.md` for instructions on surfacing any new
64+
elements or attributes added to `AndroidManifest.xml`.
65+
6166
### ApiCompat
6267

6368
There may be ApiCompat issues that need to be examined. Either fix the assembly with metadata or allow
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Text;
3+
using System.Xml.Linq;
4+
using Xamarin.SourceWriter;
5+
6+
namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;
7+
8+
static class StringExtensions
9+
{
10+
static StringExtensions ()
11+
{
12+
// micro unit testing, am so clever!
13+
if (Hyphenate ("AndSoOn") != "and-so-on")
14+
throw new InvalidOperationException ("Am so buggy 1 " + Hyphenate ("AndSoOn"));
15+
if (Hyphenate ("aBigProblem") != "a-big-problem")
16+
throw new InvalidOperationException ("Am so buggy 2");
17+
if (Hyphenate ("my-two-cents") != "my-two-cents")
18+
throw new InvalidOperationException ("Am so buggy 3");
19+
}
20+
21+
public static string Hyphenate (this string s)
22+
{
23+
var sb = new StringBuilder (s.Length * 2);
24+
for (int i = 0; i < s.Length; i++) {
25+
if (char.IsUpper (s [i])) {
26+
if (i > 0)
27+
sb.Append ('-');
28+
sb.Append (char.ToLowerInvariant (s [i]));
29+
} else
30+
sb.Append (s [i]);
31+
}
32+
return sb.ToString ();
33+
}
34+
35+
const string prefix = "AndroidManifest";
36+
37+
public static string ToActualName (this string s)
38+
{
39+
s = s.IndexOf ('.') < 0 ? s : s.Substring (s.LastIndexOf ('.') + 1);
40+
41+
var ret = (s.StartsWith (prefix, StringComparison.Ordinal) ? s.Substring (prefix.Length) : s).Hyphenate ();
42+
return ret.Length == 0 ? "manifest" : ret;
43+
}
44+
45+
public static bool GetAttributeBoolOrDefault (this XElement element, string attribute, bool defaultValue)
46+
{
47+
var value = element.Attribute (attribute)?.Value;
48+
49+
if (value is null)
50+
return defaultValue;
51+
52+
if (bool.TryParse (value, out var ret))
53+
return ret;
54+
55+
return defaultValue;
56+
}
57+
58+
public static string GetRequiredAttributeString (this XElement element, string attribute)
59+
{
60+
var value = element.Attribute (attribute)?.Value;
61+
62+
if (value is null)
63+
throw new InvalidDataException ($"Missing '{attribute}' attribute.");
64+
65+
return value;
66+
}
67+
68+
public static string GetAttributeStringOrEmpty (this XElement element, string attribute)
69+
=> element.Attribute (attribute)?.Value ?? string.Empty;
70+
71+
public static string Unhyphenate (this string s)
72+
{
73+
if (s.IndexOf ('-') < 0)
74+
return s;
75+
76+
var sb = new StringBuilder ();
77+
78+
for (var i = 0; i < s.Length; i++) {
79+
if (s [i] == '-') {
80+
sb.Append (char.ToUpper (s [i + 1]));
81+
i++;
82+
} else {
83+
sb.Append (s [i]);
84+
}
85+
}
86+
87+
return sb.ToString ();
88+
}
89+
90+
public static string Capitalize (this string s)
91+
{
92+
return char.ToUpper (s [0]) + s.Substring (1);
93+
}
94+
95+
public static void WriteAutoGeneratedHeader (this CodeWriter sw)
96+
{
97+
sw.WriteLine ("//------------------------------------------------------------------------------");
98+
sw.WriteLine ("// <auto-generated>");
99+
sw.WriteLine ("// This code was generated by 'manifest-attribute-codegen'.");
100+
sw.WriteLine ("//");
101+
sw.WriteLine ("// Changes to this file may cause incorrect behavior and will be lost if");
102+
sw.WriteLine ("// the code is regenerated.");
103+
sw.WriteLine ("// </auto-generated>");
104+
sw.WriteLine ("//------------------------------------------------------------------------------");
105+
sw.WriteLine ();
106+
sw.WriteLine ("#nullable enable"); // Roslyn turns off NRT for generated files by default, re-enable it
107+
}
108+
109+
/// <summary>
110+
/// Returns the first subset of a delimited string. ("127.0.0.1" -> "127")
111+
/// </summary>
112+
[return: NotNullIfNotNull (nameof (s))]
113+
public static string? FirstSubset (this string? s, char separator)
114+
{
115+
if (!s.HasValue ())
116+
return s;
117+
118+
var index = s.IndexOf (separator);
119+
120+
if (index < 0)
121+
return s;
122+
123+
return s.Substring (0, index);
124+
}
125+
126+
/// <summary>
127+
/// Returns the final subset of a delimited string. ("127.0.0.1" -> "1")
128+
/// </summary>
129+
[return: NotNullIfNotNull (nameof (s))]
130+
public static string? LastSubset (this string? s, char separator)
131+
{
132+
if (!s.HasValue ())
133+
return s;
134+
135+
var index = s.LastIndexOf (separator);
136+
137+
if (index < 0)
138+
return s;
139+
140+
return s.Substring (index + 1);
141+
}
142+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Xml.Linq;
2+
using Xamarin.SourceWriter;
3+
4+
namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;
5+
6+
class AttributeDefinition
7+
{
8+
public string ApiLevel { get; }
9+
public string Name { get; }
10+
public string Format { get; }
11+
public List<EnumDefinition> Enums { get; } = new List<EnumDefinition> ();
12+
13+
public AttributeDefinition (string apiLevel, string name, string format)
14+
{
15+
ApiLevel = apiLevel;
16+
Name = name;
17+
Format = format;
18+
}
19+
20+
public string GetAttributeType ()
21+
{
22+
return Format switch {
23+
"boolean" => "bool",
24+
"integer" => "int",
25+
"string" => "string?",
26+
_ => "string?",
27+
};
28+
}
29+
30+
public static AttributeDefinition FromElement (string api, XElement e)
31+
{
32+
var name = e.GetAttributeStringOrEmpty ("name");
33+
var format = e.GetAttributeStringOrEmpty ("format");
34+
35+
var def = new AttributeDefinition (api, name, format);
36+
37+
var enums = e.Elements ("enum")
38+
.Select (n => new EnumDefinition (api, n.GetAttributeStringOrEmpty ("name"), n.GetAttributeStringOrEmpty ("value")));
39+
40+
def.Enums.AddRange (enums);
41+
42+
return def;
43+
}
44+
45+
public void WriteXml (TextWriter w)
46+
{
47+
var format = Format.HasValue () ? $" format='{Format}'" : string.Empty;
48+
var api_level = int.TryParse (ApiLevel, out var level) && level <= 10 ? string.Empty : $" api-level='{ApiLevel}'";
49+
50+
w.Write ($" <a name='{Name}'{format}{api_level}");
51+
52+
if (Enums.Count > 0) {
53+
w.WriteLine (">");
54+
foreach (var e in Enums)
55+
w.WriteLine ($" <enum-definition name='{e.Name}' value='{e.Value}' api-level='{e.ApiLevel}' />");
56+
w.WriteLine (" </a>");
57+
} else
58+
w.WriteLine (" />");
59+
}
60+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Xml.Linq;
2+
3+
namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;
4+
5+
class ElementDefinition
6+
{
7+
static readonly char [] sep = [' '];
8+
9+
public string ApiLevel { get; }
10+
public string Name { get; }
11+
public string[]? Parents { get;}
12+
public List<AttributeDefinition> Attributes { get; } = new List<AttributeDefinition> ();
13+
14+
public string ActualElementName => Name.ToActualName ();
15+
16+
public ElementDefinition (string apiLevel, string name, string []? parents)
17+
{
18+
ApiLevel = apiLevel;
19+
Name = name;
20+
Parents = parents;
21+
}
22+
23+
public static ElementDefinition FromElement (string api, XElement e)
24+
{
25+
var name = e.GetAttributeStringOrEmpty ("name");
26+
var parents = e.Attribute ("parent")?.Value?.Split (sep, StringSplitOptions.RemoveEmptyEntries);
27+
var def = new ElementDefinition (api, name, parents);
28+
29+
var attrs = e.Elements ("attr")
30+
.Select (a => AttributeDefinition.FromElement (api, a));
31+
32+
def.Attributes.AddRange (attrs);
33+
34+
return def;
35+
}
36+
37+
public void WriteXml (TextWriter w)
38+
{
39+
w.WriteLine ($" <e name='{ActualElementName}' api-level='{ApiLevel}'>");
40+
41+
if (Parents?.Any () == true)
42+
foreach (var p in Parents)
43+
w.WriteLine ($" <parent>{p.ToActualName ()}</parent>");
44+
45+
foreach (var a in Attributes)
46+
a.WriteXml (w);
47+
48+
w.WriteLine (" </e>");
49+
}
50+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;
2+
3+
class EnumDefinition
4+
{
5+
public string ApiLevel { get; set; }
6+
public string Name { get; set; }
7+
public string Value { get; set; }
8+
9+
public EnumDefinition (string apiLevel, string name, string value)
10+
{
11+
ApiLevel = apiLevel;
12+
Name = name;
13+
Value = value;
14+
}
15+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Xml.Linq;
2+
3+
namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;
4+
5+
class ManifestDefinition
6+
{
7+
public string ApiLevel { get; set; } = "0";
8+
public List<ElementDefinition> Elements { get; } = new List<ElementDefinition> ();
9+
10+
// Creates a new ManifestDefinition for a single Android API from the given file path
11+
public static ManifestDefinition FromFile (string filePath)
12+
{
13+
var dir_name = new FileInfo (filePath).Directory?.Parent?.Parent?.Parent?.Name;
14+
15+
if (dir_name is null)
16+
throw new InvalidOperationException ($"Could not determine API level from {filePath}");
17+
18+
var manifest = new ManifestDefinition () {
19+
ApiLevel = dir_name.Substring (dir_name.IndexOf ('-') + 1)
20+
};
21+
22+
var elements = XDocument.Load (filePath).Root?.Elements ("declare-styleable")
23+
.Select (e => ElementDefinition.FromElement (manifest.ApiLevel, e))
24+
.ToList ();
25+
26+
if (elements is not null)
27+
manifest.Elements.AddRange (elements);
28+
29+
return manifest;
30+
}
31+
32+
public static ManifestDefinition FromSdkDirectory (string sdkPath)
33+
{
34+
// Load all the attrs_manifest.xml files from the Android SDK
35+
var manifests = Directory.GetDirectories (Path.Combine (sdkPath, "platforms"), "android-*")
36+
.Select (d => Path.Combine (d, "data", "res", "values", "attrs_manifest.xml"))
37+
.Where (File.Exists)
38+
.Order ()
39+
.Select (FromFile)
40+
.ToList ();
41+
42+
// Merge all the manifests into a single one
43+
var merged = new ManifestDefinition ();
44+
45+
foreach (var def in manifests) {
46+
foreach (var el in def.Elements) {
47+
var element = merged.Elements.FirstOrDefault (_ => _.ActualElementName == el.ActualElementName);
48+
if (element == null)
49+
merged.Elements.Add (element = new ElementDefinition (
50+
el.ApiLevel,
51+
el.Name,
52+
(string []?) el.Parents?.Clone ()
53+
));
54+
foreach (var at in el.Attributes) {
55+
var attribute = element.Attributes.FirstOrDefault (_ => _.Name == at.Name);
56+
if (attribute == null)
57+
element.Attributes.Add (attribute = new AttributeDefinition (
58+
at.ApiLevel,
59+
at.Name,
60+
at.Format
61+
));
62+
foreach (var en in at.Enums) {
63+
var enumeration = at.Enums.FirstOrDefault (_ => _.Name == en.Name);
64+
if (enumeration == null)
65+
attribute.Enums.Add (new EnumDefinition (
66+
en.ApiLevel,
67+
en.Name,
68+
en.Value
69+
));
70+
}
71+
}
72+
}
73+
}
74+
75+
return merged;
76+
}
77+
78+
public void WriteXml (TextWriter w)
79+
{
80+
w.WriteLine ("<m>");
81+
82+
foreach (var e in Elements)
83+
e.WriteXml (w);
84+
85+
w.WriteLine ("</m>");
86+
}
87+
}

0 commit comments

Comments
 (0)