-
Notifications
You must be signed in to change notification settings - Fork 531
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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.
- Loading branch information
Showing
63 changed files
with
3,673 additions
and
1,882 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
build-tools/manifest-attribute-codegen/Extensions/StringExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
60 changes: 60 additions & 0 deletions
60
build-tools/manifest-attribute-codegen/Models/AttributeDefinition.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
50
build-tools/manifest-attribute-codegen/Models/ElementDefinition.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
15
build-tools/manifest-attribute-codegen/Models/EnumDefinition.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
build-tools/manifest-attribute-codegen/Models/ManifestDefinition.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>"); | ||
} | ||
} |
Oops, something went wrong.