Skip to content

Commit

Permalink
[manifest-attribute-codegen] Automatically generate manifest attribut…
Browse files Browse the repository at this point in the history
…es from Android SDK data.
  • Loading branch information
jpobst committed Mar 7, 2024
1 parent 5205a5f commit bde026f
Show file tree
Hide file tree
Showing 39 changed files with 1,785 additions and 796 deletions.
120 changes: 120 additions & 0 deletions build-tools/manifest-attribute-codegen/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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? GetAsBoolOrNull (this XElement element, string attribute)
{
var value = element.Attribute (attribute)?.Value;

if (value is null)
return null;

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

return null;
}

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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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 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 bde026f

Please sign in to comment.