-
Notifications
You must be signed in to change notification settings - Fork 525
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
[manifest-attribute-codegen] Automatically generate manifest attributes from Android SDK data. #8781
Merged
Merged
[manifest-attribute-codegen] Automatically generate manifest attributes from Android SDK data. #8781
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May want to mention that |
||
{ | ||
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bit feels like "dead code" as nothing appears to hit this code path;
git grep '<enum-definition name'
has no matches. Perhaps this is premature?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was existing code that I didn't change:
https://github.com/xamarin/xamarin-android/blob/b521487d0226db265baaa83f0f53e2e75497c420/build-tools/manifest-attribute-codegen/manifest-attribute-codegen.cs#L165-L177
I'm not sure what the original intent was, but maybe the SDK used to specify valid values for some attributes?