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 22, 2024
1 parent 20e0fef commit 4286992
Show file tree
Hide file tree
Showing 46 changed files with 1,737 additions and 1,156 deletions.
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
@@ -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
128 changes: 75 additions & 53 deletions build-tools/manifest-attribute-codegen/Models/MetadataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,16 @@ namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator;

class MetadataSource
{
public Dictionary<string, MetadataType> Types { get; } = new ();
public Dictionary<string, MetadataElement> Elements { get; } = new ();

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

public Dictionary<string, MetadataType> Types { get; } = [];
public Dictionary<string, MetadataAttribute> Elements { get; } = [];

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.");

Elements.Add (path, new MetadataElement (path) {
Visible = element.GetAsBoolOrNull ("visible"),
Type = element.Attribute ("type")?.Value,
Name = element.Attribute ("name")?.Value,
Obsolete = element.Attribute ("obsolete")?.Value,
ReadOnly = element.GetAsBoolOrNull ("readonly") ?? false,
});
var me = new MetadataAttribute (element);
Elements.Add (me.Path, me);
}

foreach (var element in xml.Elements ("type")) {
Expand All @@ -33,83 +23,115 @@ public MetadataSource (string filename)
}
}

public MetadataElement GetMetadata (string path)
public MetadataAttribute 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;
throw new InvalidOperationException ($"No MetadataElement found for path '{path}'.");
}

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 ());
}
}

class MetadataElement
class MetadataAttribute
{
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)
public MetadataAttribute (XElement element)
{
Path = path;
Path = element.Attribute ("path")?.Value ?? throw new InvalidDataException ("Missing 'path' attribute.");

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

Visible = element.GetAttributeBoolOrDefault ("visible", true);
Type = element.Attribute ("type")?.Value;
Name = element.Attribute ("name")?.Value;
Obsolete = element.Attribute ("obsolete")?.Value;
ReadOnly = element.GetAttributeBoolOrDefault ("readonly", false);
ManualMap = element.GetAttributeBoolOrDefault ("manualMap", false);
}
}

Expand All @@ -125,8 +147,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 @@ -141,8 +162,9 @@ public MetadataType (XElement element)
Usage = element.GetRequiredAttributeString ("usage");
AllowMultiple = element.GetAttributeBoolOrDefault ("allowMultiple", false);
IsJniNameProvider = element.GetAttributeBoolOrDefault ("jniNameProvider", false);
HasDefaultConstructor = element.GetAttributeBoolOrDefault("defaultConstructor", true);
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;
}
}
Loading

0 comments on commit 4286992

Please sign in to comment.