Skip to content

Commit

Permalink
Generate mappings used by Xamarin.Android.Build.Tasks.
Browse files Browse the repository at this point in the history
  • Loading branch information
jpobst committed Mar 8, 2024
1 parent bde026f commit d544aca
Show file tree
Hide file tree
Showing 44 changed files with 1,615 additions and 1,134 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Xml.Linq;
using Xamarin.SourceWriter;
Expand Down Expand Up @@ -41,19 +42,6 @@ public static string ToActualName (this string s)
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;
Expand Down Expand Up @@ -117,4 +105,38 @@ public static void WriteAutoGeneratedHeader (this CodeWriter sw)
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
Expand Up @@ -17,6 +17,16 @@ public AttributeDefinition (string apiLevel, string name, string format)
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");
Expand Down
97 changes: 61 additions & 36 deletions build-tools/manifest-attribute-codegen/Models/MetadataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ class MetadataSource

static readonly MetadataElement default_element = new MetadataElement ("*");


public MetadataSource (string filename)
{
var xml = XElement.Load (filename);

foreach (var element in xml.Elements ("element")) {
var path = element.Attribute ("path")?.Value ?? throw new InvalidDataException ("Missing 'path' attribute.");

if (!path.Contains ('.'))
throw new InvalidDataException ($"Invalid 'path' attribute value: {path}");

Elements.Add (path, new MetadataElement (path) {
Visible = element.GetAsBoolOrNull ("visible"),
Visible = element.GetAttributeBoolOrDefault ("visible", true),
Type = element.Attribute ("type")?.Value,
Name = element.Attribute ("name")?.Value,
Obsolete = element.Attribute ("obsolete")?.Value,
ReadOnly = element.GetAsBoolOrNull ("readonly") ?? false,
ReadOnly = element.GetAttributeBoolOrDefault("readonly", false),
ManualMap = element.GetAttributeBoolOrDefault ("manualMap", false),
});
}

Expand All @@ -35,63 +38,85 @@ public MetadataSource (string filename)

public MetadataElement GetMetadata (string path)
{
if (Elements.TryGetValue (path, out var element)) {
element.Consumed = true;
if (Elements.TryGetValue (path, out var element))
return element;
}

return default_element;
}

public void EnsureMetadataElementsConsumed ()
public void EnsureAllElementsAccountedFor (List<ElementDefinition> elements)
{
var unconsumed = Elements.Values.Where (e => !e.Consumed).ToList ();

if (unconsumed.Count == 0)
return;
var missing = new List<string> ();

var sb = new StringBuilder ();
sb.AppendLine ("The following metadata elements were not consumed:");
foreach (var e in elements) {
if (!Types.TryGetValue (e.ActualElementName, out var t)) {
missing.Add ($"- Type: <{e.ActualElementName}>");
continue;
}

foreach (var e in unconsumed)
sb.AppendLine ($"- {e.Path}");
if (t.Ignore)
continue;

throw new InvalidOperationException (sb.ToString ());
}
foreach (var a in e.Attributes) {
var name = $"{e.ActualElementName}.{a.Name}";

public void EnsureMetadataTypesConsumed ()
{
var unconsumed = Types.Values.Where (t => !t.Consumed && !t.Ignore).ToList ();
if (!Elements.TryGetValue (name, out _))
missing.Add ($"- Element: {name}");
}
}

if (unconsumed.Count == 0)
if (missing.Count == 0)
return;

var sb = new StringBuilder ();
sb.AppendLine ("The following metadata types were not consumed:");
sb.AppendLine ("The following manifest elements are not specified in the metadata:");

foreach (var t in unconsumed)
sb.AppendLine ($"- {t.Name}");
foreach (var m in missing)
sb.AppendLine (m);

throw new InvalidOperationException (sb.ToString ());
}

public void EnsureAllTypesAccountedFor (IEnumerable<ElementDefinition> elements)
public void EnsureAllMetadataElementsExistInManifest (List<ElementDefinition> elements)
{
var missing = new List<string> ();

foreach (var e in elements) {
if (!Types.ContainsKey (e.ActualElementName))
missing.Add (e.ActualElementName);
foreach (var type in Types) {
var type_def = elements.FirstOrDefault (e => e.ActualElementName == type.Key);

if (type_def is null) {
missing.Add ($"- Type: {type.Key}");
continue;
}
}

foreach (var type in Elements) {
var type_name = type.Key.FirstSubset ('.');
var elem_name = type.Key.LastSubset ('.');

var type_def = elements.FirstOrDefault (e => e.ActualElementName == type_name);

if (type_def is null) {
missing.Add ($"- Element: {type.Key}");
continue;
}

var elem_def = type_def.Attributes.FirstOrDefault (e => e.Name == elem_name);

if (elem_def is null) {
missing.Add ($"- Element: {type.Key}");
continue;
}
}

if (missing.Count == 0)
return;

var sb = new StringBuilder ();
sb.AppendLine ("The following types were not accounted for:");
sb.AppendLine ("The following elements specified in the metadata were not found in the manifest:");

foreach (var m in missing.Order ())
sb.AppendLine ($"- {m}");
foreach (var e in missing)
sb.AppendLine (e);

throw new InvalidOperationException (sb.ToString ());
}
Expand All @@ -100,12 +125,12 @@ public void EnsureAllTypesAccountedFor (IEnumerable<ElementDefinition> elements)
class MetadataElement
{
public string Path { get; set; }
public bool? Visible { get; set; }
public bool Visible { get; set; } = true;
public string? Type { get; set; }
public string? Name { get; set; }
public string? Obsolete { get; set; }
public bool ReadOnly { get; set; }
public bool Consumed { get; set; }
public bool ManualMap { get; set; }

public MetadataElement (string path)
{
Expand All @@ -125,8 +150,7 @@ public class MetadataType
public bool IsJniNameProvider { get; set; }
public bool HasDefaultConstructor { get; set; }
public bool IsSealed { get; set; }
public bool Consumed { get; set; }

public bool GenerateMapping { get; set; }

public MetadataType (XElement element)
{
Expand All @@ -143,6 +167,7 @@ public MetadataType (XElement element)
IsJniNameProvider = element.GetAttributeBoolOrDefault ("jniNameProvider", false);
HasDefaultConstructor = element.GetAttributeBoolOrDefault("defaultConstructor", true);
IsSealed = element.GetAttributeBoolOrDefault ("sealed", true);
ManagedName = element.Attribute ("managedName")?.Value ?? Name.Unhyphenate ().Capitalize () + "Attribute";
ManagedName = element.Attribute ("managedName")?.Value ?? Name.Unhyphenate ().Capitalize () + "Attribute";
GenerateMapping = element.GetAttributeBoolOrDefault ("generateMapping", true);
}
}
14 changes: 7 additions & 7 deletions build-tools/manifest-attribute-codegen/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ public static int Main (string [] args)
// Read metadata file
var metadata = new MetadataSource (metadata_file);

// Ensure everything in the Android SDK is accounted for.
// This forces us to handle anything new that's been added to the SDK.
metadata.EnsureAllElementsAccountedFor (merged.Elements);

// Ensure there are no unused elements in the metadata file
metadata.EnsureAllMetadataElementsExistInManifest (merged.Elements);

// Generate manifest attributes C# code
foreach (var type in metadata.Types.Values.Where (t => !t.Ignore)) {
using var w = new StreamWriter (Path.Combine (base_dir, type.OutputFile));
Expand All @@ -83,13 +90,6 @@ public static int Main (string [] args)
writer.Write (cw);
}

// Ensure everything we found in the Android SDK is accounted for.
// This forces us to handle anything new that's been added to the SDK.
// metadata.EnsureAllTypesExist (merged.Elements);
metadata.EnsureAllTypesAccountedFor (merged.Elements);
metadata.EnsureMetadataTypesConsumed ();
metadata.EnsureMetadataElementsConsumed ();

return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;
// This is the common data class used by both Mono.Android and Xamarin.Android.Build.Tasks.
class AttributeDataClass : ClassWriter
{
AttributeMappingField? mapping_field;
AttributeMappingStaticConstructor? static_constructor;

static AttributeMappingManualInitializer manual_mapping_partial = AttributeMappingManualInitializer.Create ();

public string Namespace { get; set; }

public AttributeDataClass (string ns)
Expand All @@ -14,8 +19,6 @@ public AttributeDataClass (string ns)

public static AttributeDataClass Create (ElementDefinition attr, MetadataSource metadata, MetadataType type)
{
type.Consumed = true;

var c = new AttributeDataClass (type.Namespace) {
Name = type.ManagedName,
IsPublic = true,
Expand All @@ -41,12 +44,12 @@ public static AttributeDataClass Create (ElementDefinition attr, MetadataSource
foreach (var a in attr.Attributes.OrderBy (a => a.Name)) {
var attr_metadata = metadata.GetMetadata ($"{attr.ActualElementName}.{a.Name}");

if (attr_metadata.Visible == false)
if (!attr_metadata.Visible)
continue;

var p = new PropertyWriter {
Name = (attr_metadata.Name ?? a.Name).Capitalize (),
PropertyType = new TypeReferenceWriter (attr_metadata.Type ?? GetAttributeType (a)),
PropertyType = new TypeReferenceWriter (attr_metadata.Type ?? a.GetAttributeType ()),
IsPublic = true,
HasGet = true,
HasSet = true,
Expand All @@ -61,17 +64,13 @@ public static AttributeDataClass Create (ElementDefinition attr, MetadataSource
c.Properties.Add (p);
}

return c;
}
// Create mapping field used by Xamarin.Android.Build.Tasks
if (type.GenerateMapping) {
c.mapping_field = AttributeMappingField.Create (type);
c.static_constructor = AttributeMappingStaticConstructor.Create (attr, metadata, type);
}

static string GetAttributeType (AttributeDefinition attr)
{
return attr.Format switch {
"boolean" => "bool",
"integer" => "int",
"string" => "string?",
_ => "string?",
};
return c;
}

public override void Write (CodeWriter writer)
Expand All @@ -86,4 +85,19 @@ public override void Write (CodeWriter writer)

base.Write (writer);
}

public override void WriteMembers (CodeWriter writer)
{
base.WriteMembers (writer);

if (mapping_field is not null) {
writer.WriteLineNoIndent ("#if XABT_MANIFEST_EXTENSIONS");
mapping_field?.Write (writer);
writer.WriteLine ();
static_constructor?.Write (writer);
writer.WriteLine ();
manual_mapping_partial?.Write (writer);
writer.WriteLineNoIndent ("#endif");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Xamarin.SourceWriter;

namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;

class AttributeMappingField : FieldWriter
{
public static AttributeMappingField Create (MetadataType type)
{
var field = new AttributeMappingField {
Name = "mapping",
IsStatic = true,
Type = new TypeReferenceWriter ($"Xamarin.Android.Manifest.ManifestDocumentElement<{type.ManagedName}>"),
Value = $"new (\"{type.Name}\")",
};

return field;
}
}
Loading

0 comments on commit d544aca

Please sign in to comment.