Skip to content
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 4 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Documentation/workflow/HowToAddNewApiLevel.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ the more manual parts like enumification that will likely change as the APIs mat
- Add new level to `/build-tools/xaprepare/xaprepare/ConfigAndData/Dependencies/AndroidToolchain.cs`:
- `new AndroidPlatformComponent ("platform-S_r01", apiLevel: "S", pkgRevision: "1"),`

At this point, you can run `Xamarin.Android.sln /t:Prepare` using your usual mechanism, and
At this point, you can run `Xamarin.Android.sln -t:Prepare` using your usual mechanism, and
the new platform will be downloaded to your local Android SDK.

### Generate `params.txt` File
Expand Down Expand Up @@ -49,6 +49,11 @@ the new platform will be downloaded to your local Android SDK.
- 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
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) {
Copy link
Member

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?

Copy link
Contributor Author

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?

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 (" />");
}
}
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May want to mention that filePath is expected to be similar to $HOME/android-toolchain/sdk/platforms/android-34/data/res/values/attrs_manifest.xml, and thus dir_name would be android-34, and elements + //declare-styleable is coming from attrs_manifest.xml.

{
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
Loading